diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-02 09:09:24 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-02 09:09:24 +0300 |
commit | 8bf623d6eaa873c2a77da3c14b51ce1eabdc84f7 (patch) | |
tree | a42c566ca98ae0eee3201994860ba31fe46bc45f | |
parent | 123582839259a70910bd0e9109e57ed65d71cb23 (diff) |
Add latest changes from gitlab-org/gitlab@master
17 files changed, 597 insertions, 46 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index 25c3b79f522..48e7a92d05f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -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/**/*' diff --git a/.rubocop_todo/cop/experiments_test_coverage.yml b/.rubocop_todo/cop/experiments_test_coverage.yml new file mode 100644 index 00000000000..a64ea13b743 --- /dev/null +++ b/.rubocop_todo/cop/experiments_test_coverage.yml @@ -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' diff --git a/app/assets/javascripts/ci/reports/components/report_item.vue b/app/assets/javascripts/ci/reports/components/report_item.vue index ca1f3301691..7342df6da67 100644 --- a/app/assets/javascripts/ci/reports/components/report_item.vue +++ b/app/assets/javascripts/ci/reports/components/report_item.vue @@ -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" diff --git a/app/assets/javascripts/import_entities/components/group_dropdown.vue b/app/assets/javascripts/import_entities/components/group_dropdown.vue index 1c31c04a416..68bdcf7ef90 100644 --- a/app/assets/javascripts/import_entities/components/group_dropdown.vue +++ b/app/assets/javascripts/import_entities/components/group_dropdown.vue @@ -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({ diff --git a/app/assets/javascripts/import_entities/components/import_target_dropdown.vue b/app/assets/javascripts/import_entities/components/import_target_dropdown.vue new file mode 100644 index 00000000000..b18a106608a --- /dev/null +++ b/app/assets/javascripts/import_entities/components/import_target_dropdown.vue @@ -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> diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue index 735939f991f..ed390f5ef58 100644 --- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue +++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue @@ -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> diff --git a/doc/ci/testing/test_coverage_visualization.md b/doc/ci/testing/test_coverage_visualization.md index 669157daa21..613cefe1a0d 100644 --- a/doc/ci/testing/test_coverage_visualization.md +++ b/doc/ci/testing/test_coverage_visualization.md @@ -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`: diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb index f58d672efe8..3b044f8a051 100644 --- a/qa/qa/page/admin/menu.rb +++ b/qa/qa/page/admin/menu.rb @@ -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? diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb index 898bf78b46a..57c6484dc13 100644 --- a/qa/qa/page/project/import/github.rb +++ b/qa/qa/page/project/import/github.rb @@ -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 diff --git a/rubocop/cop/experiments_test_coverage.rb b/rubocop/cop/experiments_test_coverage.rb new file mode 100644 index 00000000000..4bb2832030c --- /dev/null +++ b/rubocop/cop/experiments_test_coverage.rb @@ -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 diff --git a/spec/frontend/import_entities/components/import_target_dropdown_spec.js b/spec/frontend/import_entities/components/import_target_dropdown_spec.js new file mode 100644 index 00000000000..c12baed2374 --- /dev/null +++ b/spec/frontend/import_entities/components/import_target_dropdown_spec.js @@ -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' }, + ]); + }); +}); diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js index 57e232a4c46..9e4838ed020 100644 --- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js +++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js @@ -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); diff --git a/spec/frontend/import_entities/mock_data.js b/spec/frontend/import_entities/mock_data.js new file mode 100644 index 00000000000..9208f99651f --- /dev/null +++ b/spec/frontend/import_entities/mock_data.js @@ -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'; diff --git a/spec/lib/backup/database_spec.rb b/spec/lib/backup/database_spec.rb index 5d53ad9fe22..61e6c59a1a5 100644 --- a/spec/lib/backup/database_spec.rb +++ b/spec/lib/backup/database_spec.rb @@ -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' diff --git a/spec/rubocop/cop/experiments_test_coverage_spec.rb b/spec/rubocop/cop/experiments_test_coverage_spec.rb new file mode 100644 index 00000000000..eb1e672ef40 --- /dev/null +++ b/spec/rubocop/cop/experiments_test_coverage_spec.rb @@ -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 diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 23df8d4ef85..414ce01917b 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -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' diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb index b12f2b839af..344429dc6ec 100644 --- a/spec/tasks/gitlab/db_rake_spec.rb +++ b/spec/tasks/gitlab/db_rake_spec.rb @@ -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' |