From 1ba682300fb97a96de47cc5b261f6df93ca78bd0 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 12 Sep 2023 15:12:04 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .../user_creates_feature_flag_spec.rb | 29 ++- .../components/new_environments_dropdown_spec.js | 80 +++---- .../feature_flags/components/strategy_spec.js | 13 +- .../components/blob_viewers/rich_viewer_spec.js | 10 +- .../components/markdown/field_view_spec.js | 22 +- spec/graphql/types/ci/job_type_spec.rb | 1 + spec/helpers/sidekiq_helper_spec.rb | 2 +- .../prometheus/additional_metrics_parser_spec.rb | 248 --------------------- spec/models/concerns/issuable_spec.rb | 15 ++ spec/models/concerns/transitionable_spec.rb | 42 ++++ spec/models/merge_request_spec.rb | 36 +++ .../prometheus_metric_spec.rb | 67 ------ .../prometheus_panel_spec.rb | 74 ------ spec/requests/api/graphql/ci/jobs_spec.rb | 103 ++++++++- 14 files changed, 292 insertions(+), 450 deletions(-) delete mode 100644 spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb create mode 100644 spec/models/concerns/transitionable_spec.rb delete mode 100644 spec/models/performance_monitoring/prometheus_metric_spec.rb delete mode 100644 spec/models/performance_monitoring/prometheus_panel_spec.rb (limited to 'spec') diff --git a/spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb b/spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb index 4af5c91479a..127610cf4db 100644 --- a/spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb +++ b/spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb @@ -7,13 +7,14 @@ RSpec.describe 'User creates feature flag', :js do let(:user) { create(:user) } let(:project) { create(:project, namespace: user.namespace) } + let!(:environment) { create(:environment, :production, project: project) } before do project.add_developer(user) sign_in(user) end - it 'user creates a flag enabled for user ids' do + it 'user creates a flag enabled for user ids with existing environment' do visit(new_project_feature_flag_path(project)) set_feature_flag_info('test_feature', 'Test feature') within_strategy_row(1) do @@ -29,6 +30,22 @@ RSpec.describe 'User creates feature flag', :js do expect(page).to have_text('test_feature') end + it 'user creates a flag enabled for user ids with non-existing environment' do + visit(new_project_feature_flag_path(project)) + set_feature_flag_info('test_feature', 'Test feature') + within_strategy_row(1) do + select 'User IDs', from: 'Type' + fill_in 'User IDs', with: 'user1, user2' + environment_plus_button.click + environment_search_input.set('foo-bar') + environment_search_create_button.first.click + end + click_button 'Create feature flag' + + expect_user_to_see_feature_flags_index_page + expect(page).to have_text('test_feature') + end + it 'user creates a flag with default environment scopes' do visit(new_project_feature_flag_path(project)) set_feature_flag_info('test_flag', 'Test flag') @@ -74,14 +91,18 @@ RSpec.describe 'User creates feature flag', :js do end def environment_plus_button - find('.js-new-environments-dropdown') + find('[data-testid=new-environments-dropdown]') end def environment_search_input - find('.js-new-environments-dropdown input') + find('[data-testid=new-environments-dropdown] input') end def environment_search_results - all('.js-new-environments-dropdown button.dropdown-item') + all('[data-testid=new-environments-dropdown] li') + end + + def environment_search_create_button + all('[data-testid=new-environments-dropdown] button') end end diff --git a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js index 6156addd63f..b503a6f829e 100644 --- a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js +++ b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js @@ -1,7 +1,6 @@ -import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlCollapsibleListbox } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; -import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import NewEnvironmentsDropdown from '~/feature_flags/components/new_environments_dropdown.vue'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; @@ -13,87 +12,78 @@ describe('New Environments Dropdown', () => { let wrapper; let axiosMock; - beforeEach(() => { + const createWrapper = (axiosResult = []) => { axiosMock = new MockAdapter(axios); - wrapper = shallowMount(NewEnvironmentsDropdown, { + axiosMock.onGet(TEST_HOST).reply(HTTP_STATUS_OK, axiosResult); + + wrapper = shallowMountExtended(NewEnvironmentsDropdown, { provide: { environmentsEndpoint: TEST_HOST }, + stubs: { + GlCollapsibleListbox, + }, }); - }); + }; + + const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); + const findCreateEnvironmentButton = () => wrapper.findByTestId('add-environment-button'); afterEach(() => { axiosMock.restore(); }); describe('before results', () => { + beforeEach(() => { + createWrapper(); + }); + it('should show a loading icon', () => { - axiosMock.onGet(TEST_HOST).reply(() => { - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - }); - wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus'); - return axios.waitForAll(); + expect(findListbox().props('searching')).toBe(true); }); it('should not show any dropdown items', () => { - axiosMock.onGet(TEST_HOST).reply(() => { - expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(0); - }); - wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus'); - return axios.waitForAll(); + expect(findListbox().props('items')).toEqual([]); }); }); describe('with empty results', () => { - let item; beforeEach(async () => { - axiosMock.onGet(TEST_HOST).reply(HTTP_STATUS_OK, []); - wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus'); - wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', TEST_SEARCH); + createWrapper(); + findListbox().vm.$emit('search', TEST_SEARCH); await axios.waitForAll(); - await nextTick(); - item = wrapper.findComponent(GlDropdownItem); }); it('should display a Create item label', () => { - expect(item.text()).toBe('Create production'); - }); - - it('should display that no matching items are found', () => { - expect(wrapper.findComponent({ ref: 'noResults' }).exists()).toBe(true); + expect(findCreateEnvironmentButton().text()).toBe(`Create ${TEST_SEARCH}`); }); it('should emit a new scope when selected', () => { - item.vm.$emit('click'); + findCreateEnvironmentButton().vm.$emit('click'); expect(wrapper.emitted('add')).toEqual([[TEST_SEARCH]]); }); }); describe('with results', () => { - let items; - beforeEach(() => { - axiosMock.onGet(TEST_HOST).reply(HTTP_STATUS_OK, ['prod', 'production']); - wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus'); - wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'prod'); - return axios.waitForAll().then(() => { - items = wrapper.findAllComponents(GlDropdownItem); - }); + beforeEach(async () => { + createWrapper(['prod', 'production']); + findListbox().vm.$emit('search', TEST_SEARCH); + await axios.waitForAll(); }); - it('should display one item per result', () => { - expect(items).toHaveLength(2); + it('should populate results properly', () => { + expect(findListbox().props().items).toHaveLength(2); }); - it('should emit an add if an item is clicked', () => { - items.at(0).vm.$emit('click'); + it('should emit an add on selection', () => { + findListbox().vm.$emit('select', ['prod']); expect(wrapper.emitted('add')).toEqual([['prod']]); }); - it('should not display a create label', () => { - items = items.filter((i) => i.text().startsWith('Create')); - expect(items).toHaveLength(0); - }); - it('should not display a message about no results', () => { expect(wrapper.findComponent({ ref: 'noResults' }).exists()).toBe(false); }); + + it('should not display a footer with the create button', () => { + expect(findCreateEnvironmentButton().exists()).toBe(false); + }); }); }); diff --git a/spec/frontend/feature_flags/components/strategy_spec.js b/spec/frontend/feature_flags/components/strategy_spec.js index ca6e338ac6c..90021829212 100644 --- a/spec/frontend/feature_flags/components/strategy_spec.js +++ b/spec/frontend/feature_flags/components/strategy_spec.js @@ -1,11 +1,14 @@ import { GlAlert, GlFormSelect, GlLink, GlToken, GlButton } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; import Vue, { nextTick } from 'vue'; import { last } from 'lodash'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import Api from '~/api'; +import axios from '~/lib/utils/axios_utils'; import NewEnvironmentsDropdown from '~/feature_flags/components/new_environments_dropdown.vue'; +import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import Strategy from '~/feature_flags/components/strategy.vue'; import StrategyParameters from '~/feature_flags/components/strategy_parameters.vue'; import { @@ -22,16 +25,18 @@ import { userList } from '../mock_data'; jest.mock('~/api'); +const TEST_HOST = '/test'; const provide = { strategyTypeDocsPagePath: 'link-to-strategy-docs', environmentsScopeDocsPath: 'link-scope-docs', - environmentsEndpoint: '', + environmentsEndpoint: TEST_HOST, }; Vue.use(Vuex); describe('Feature flags strategy', () => { let wrapper; + let axiosMock; const findStrategyParameters = () => wrapper.findComponent(StrategyParameters); const findDocsLinks = () => wrapper.findAllComponents(GlLink); @@ -45,6 +50,8 @@ describe('Feature flags strategy', () => { provide, }, ) => { + axiosMock = new MockAdapter(axios); + axiosMock.onGet(TEST_HOST).reply(HTTP_STATUS_OK, []); wrapper = mount(Strategy, { store: createStore({ projectId: '1' }), ...opts }); }; @@ -52,6 +59,10 @@ describe('Feature flags strategy', () => { Api.searchFeatureFlagUserLists.mockResolvedValue({ data: [userList] }); }); + afterEach(() => { + axiosMock.restore(); + }); + describe('helper links', () => { const propsData = { strategy: {}, index: 0, userLists: [userList] }; factory({ propsData, provide }); diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js index 6a3337aa046..eadcd452929 100644 --- a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js +++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js @@ -29,6 +29,8 @@ describe('Blob Rich Viewer component', () => { beforeEach(() => createComponent()); + const findMarkdownFieldView = () => wrapper.findComponent(MarkdownFieldView); + describe('Markdown content', () => { const generateDummyContent = (contentLength) => { let generatedContent = ''; @@ -48,14 +50,17 @@ describe('Blob Rich Viewer component', () => { expect(wrapper.text()).toContain('Line: 10'); expect(wrapper.text()).not.toContain('Line: 50'); expect(wrapper.emitted(CONTENT_LOADED_EVENT)).toBeUndefined(); + expect(findMarkdownFieldView().props('isLoading')).toBe(true); }); - it('renders the rest of the file later and emits a content loaded event', () => { + it('renders the rest of the file later and emits a content loaded event', async () => { jest.runAllTimers(); + await nextTick(); expect(wrapper.text()).toContain('Line: 10'); expect(wrapper.text()).toContain('Line: 50'); expect(wrapper.emitted(CONTENT_LOADED_EVENT)).toHaveLength(1); + expect(findMarkdownFieldView().props('isLoading')).toBe(false); }); it('sanitizes the content', () => { @@ -72,6 +77,7 @@ describe('Blob Rich Viewer component', () => { it('renders the entire file immediately and emits a content loaded event', () => { expect(wrapper.text()).toContain('Line: 5'); expect(wrapper.emitted(CONTENT_LOADED_EVENT)).toHaveLength(1); + expect(findMarkdownFieldView().props('isLoading')).toBe(false); }); it('sanitizes the content', () => { @@ -97,7 +103,7 @@ describe('Blob Rich Viewer component', () => { }); it('is using Markdown View Field', () => { - expect(wrapper.findComponent(MarkdownFieldView).exists()).toBe(true); + expect(findMarkdownFieldView().exists()).toBe(true); }); it('scrolls to the hash location', () => { diff --git a/spec/frontend/vue_shared/components/markdown/field_view_spec.js b/spec/frontend/vue_shared/components/markdown/field_view_spec.js index 1bbbe0896f2..f61c67c4f9b 100644 --- a/spec/frontend/vue_shared/components/markdown/field_view_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_view_spec.js @@ -6,15 +6,27 @@ import { renderGFM } from '~/behaviors/markdown/render_gfm'; jest.mock('~/behaviors/markdown/render_gfm'); describe('Markdown Field View component', () => { - function createComponent() { - shallowMount(MarkdownFieldView); + function createComponent(isLoading = false) { + shallowMount(MarkdownFieldView, { propsData: { isLoading } }); } - beforeEach(() => { + it('processes rendering with GFM', () => { createComponent(); - }); - it('processes rendering with GFM', () => { expect(renderGFM).toHaveBeenCalledTimes(1); }); + + describe('watchers', () => { + it('does not process rendering with GFM if isLoading is true', () => { + createComponent(true); + + expect(renderGFM).not.toHaveBeenCalled(); + }); + + it('processes rendering with GFM when isLoading is updated to `false`', () => { + createComponent(false); + + expect(renderGFM).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb index f31c0d5255c..a69c6f37ee1 100644 --- a/spec/graphql/types/ci/job_type_spec.rb +++ b/spec/graphql/types/ci/job_type_spec.rb @@ -32,6 +32,7 @@ RSpec.describe Types::Ci::JobType, feature_category: :continuous_integration do needs pipeline playable + previousStageJobs previousStageJobsOrNeeds project queued_at diff --git a/spec/helpers/sidekiq_helper_spec.rb b/spec/helpers/sidekiq_helper_spec.rb index 6a0a92bafd8..594996bac95 100644 --- a/spec/helpers/sidekiq_helper_spec.rb +++ b/spec/helpers/sidekiq_helper_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe SidekiqHelper do +RSpec.describe SidekiqHelper, feature_category: :shared do describe 'parse_sidekiq_ps' do it 'parses line with time' do line = '55137 10,0 2,1 S+ 2:30pm sidekiq 4.1.4 gitlab [0 of 25 busy] ' diff --git a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb deleted file mode 100644 index 559557f9313..00000000000 --- a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb +++ /dev/null @@ -1,248 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Prometheus::AdditionalMetricsParser do - include Prometheus::MetricBuilders - - let(:parser_error_class) { Gitlab::Prometheus::ParsingError } - - describe '#load_groups_from_yaml' do - subject { described_class.load_groups_from_yaml('dummy.yaml') } - - describe 'parsing sample yaml' do - let(:sample_yaml) do - <<-EOF.strip_heredoc - - group: group_a - priority: 1 - metrics: - - title: "title" - required_metrics: [ metric_a, metric_b ] - weight: 1 - queries: [{ query_range: 'query_range_a', label: label, unit: unit }] - - title: "title" - required_metrics: [metric_a] - weight: 1 - queries: [{ query_range: 'query_range_empty' }] - - group: group_b - priority: 1 - metrics: - - title: title - required_metrics: ['metric_a'] - weight: 1 - queries: [{query_range: query_range_a}] - EOF - end - - before do - allow(described_class).to receive(:load_yaml_file) { YAML.safe_load(sample_yaml) } - end - - it 'parses to two metric groups with 2 and 1 metric respectively' do - expect(subject.count).to eq(2) - expect(subject[0].metrics.count).to eq(2) - expect(subject[1].metrics.count).to eq(1) - end - - it 'provide group data' do - expect(subject[0]).to have_attributes(name: 'group_a', priority: 1) - expect(subject[1]).to have_attributes(name: 'group_b', priority: 1) - end - - it 'provides metrics data' do - metrics = subject.flat_map(&:metrics) - - expect(metrics.count).to eq(3) - expect(metrics[0]).to have_attributes(title: 'title', required_metrics: %w(metric_a metric_b), weight: 1) - expect(metrics[1]).to have_attributes(title: 'title', required_metrics: %w(metric_a), weight: 1) - expect(metrics[2]).to have_attributes(title: 'title', required_metrics: %w{metric_a}, weight: 1) - end - - it 'provides query data' do - queries = subject.flat_map(&:metrics).flat_map(&:queries) - - expect(queries.count).to eq(3) - expect(queries[0]).to eq(query_range: 'query_range_a', label: 'label', unit: 'unit') - expect(queries[1]).to eq(query_range: 'query_range_empty') - expect(queries[2]).to eq(query_range: 'query_range_a') - end - end - - shared_examples 'required field' do |field_name| - context "when #{field_name} is nil" do - before do - allow(described_class).to receive(:load_yaml_file) { YAML.safe_load(field_missing) } - end - - it 'throws parsing error' do - expect { subject }.to raise_error(parser_error_class, /#{field_name} can't be blank/i) - end - end - - context "when #{field_name} are not specified" do - before do - allow(described_class).to receive(:load_yaml_file) { YAML.safe_load(field_nil) } - end - - it 'throws parsing error' do - expect { subject }.to raise_error(parser_error_class, /#{field_name} can't be blank/i) - end - end - end - - describe 'group required fields' do - it_behaves_like 'required field', 'metrics' do - let(:field_nil) do - <<-EOF.strip_heredoc - - group: group_a - priority: 1 - metrics: - EOF - end - - let(:field_missing) do - <<-EOF.strip_heredoc - - group: group_a - priority: 1 - EOF - end - end - - it_behaves_like 'required field', 'name' do - let(:field_nil) do - <<-EOF.strip_heredoc - - group: - priority: 1 - metrics: [] - EOF - end - - let(:field_missing) do - <<-EOF.strip_heredoc - - priority: 1 - metrics: [] - EOF - end - end - - it_behaves_like 'required field', 'priority' do - let(:field_nil) do - <<-EOF.strip_heredoc - - group: group_a - priority: - metrics: [] - EOF - end - - let(:field_missing) do - <<-EOF.strip_heredoc - - group: group_a - metrics: [] - EOF - end - end - end - - describe 'metrics fields parsing' do - it_behaves_like 'required field', 'title' do - let(:field_nil) do - <<-EOF.strip_heredoc - - group: group_a - priority: 1 - metrics: - - title: - required_metrics: [] - weight: 1 - queries: [] - EOF - end - - let(:field_missing) do - <<-EOF.strip_heredoc - - group: group_a - priority: 1 - metrics: - - required_metrics: [] - weight: 1 - queries: [] - EOF - end - end - - it_behaves_like 'required field', 'required metrics' do - let(:field_nil) do - <<-EOF.strip_heredoc - - group: group_a - priority: 1 - metrics: - - title: title - required_metrics: - weight: 1 - queries: [] - EOF - end - - let(:field_missing) do - <<-EOF.strip_heredoc - - group: group_a - priority: 1 - metrics: - - title: title - weight: 1 - queries: [] - EOF - end - end - - it_behaves_like 'required field', 'weight' do - let(:field_nil) do - <<-EOF.strip_heredoc - - group: group_a - priority: 1 - metrics: - - title: title - required_metrics: [] - weight: - queries: [] - EOF - end - - let(:field_missing) do - <<-EOF.strip_heredoc - - group: group_a - priority: 1 - metrics: - - title: title - required_metrics: [] - queries: [] - EOF - end - end - - it_behaves_like 'required field', :queries do - let(:field_nil) do - <<-EOF.strip_heredoc - - group: group_a - priority: 1 - metrics: - - title: title - required_metrics: [] - weight: 1 - queries: - EOF - end - - let(:field_missing) do - <<-EOF.strip_heredoc - - group: group_a - priority: 1 - metrics: - - title: title - required_metrics: [] - weight: 1 - EOF - end - end - end - end -end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index c6f3cfc7b8a..705f8f46a90 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -626,6 +626,21 @@ RSpec.describe Issuable, feature_category: :team_planning do end end + describe "#importing_or_transitioning?" do + let(:merge_request) { build(:merge_request, transitioning: transitioning, importing: importing) } + + where(:transitioning, :importing, :result) do + true | false | true + false | true | true + true | true | true + false | false | false + end + + with_them do + it { expect(merge_request.importing_or_transitioning?).to eq(result) } + end + end + describe '#labels_array' do let(:project) { create(:project) } let(:bug) { create(:label, project: project, title: 'bug') } diff --git a/spec/models/concerns/transitionable_spec.rb b/spec/models/concerns/transitionable_spec.rb new file mode 100644 index 00000000000..a1f011ff72e --- /dev/null +++ b/spec/models/concerns/transitionable_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Transitionable, feature_category: :code_review_workflow do + let(:klass) do + Class.new do + include Transitionable + + def initialize(transitioning) + @transitioning = transitioning + end + + def project + Project.new + end + end + end + + let(:object) { klass.new(transitioning) } + + describe 'For a class' do + using RSpec::Parameterized::TableSyntax + + describe '#transitioning?' do + where(:transitioning, :feature_flag, :result) do + true | true | true + false | false | false + true | false | false + false | true | false + end + + with_them do + before do + stub_feature_flags(skip_validations_during_transitions: feature_flag) + end + + it { expect(object.transitioning?).to eq(result) } + end + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 3a71ec01b5c..2728c9ae72b 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -360,6 +360,23 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev end end + describe "#validate_reviewer_size_length" do + let(:merge_request) { build(:merge_request, transitioning: transitioning) } + + where(:transitioning, :to_or_not_to) do + false | :to + true | :not_to + end + + with_them do + it do + expect(merge_request).send(to_or_not_to, receive(:validate_reviewer_size_length)) + + merge_request.valid? + end + end + end + describe '#validate_target_project' do let(:merge_request) do build(:merge_request, source_project: project, target_project: project, importing: importing) @@ -386,6 +403,23 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev it { expect(merge_request.valid?(false)).to eq true } end end + + context "with the skip_validations_during_transition_feature_flag" do + let(:merge_request) { build(:merge_request, transitioning: transitioning) } + + where(:transitioning, :to_or_not_to) do + false | :to + true | :not_to + end + + with_them do + it do + expect(merge_request).send(to_or_not_to, receive(:validate_target_project)) + + merge_request.valid? + end + end + end end end @@ -4487,6 +4521,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev shared_examples 'for an invalid state transition' do specify 'is not a valid state transition' do expect { transition! }.to raise_error(StateMachines::InvalidTransition) + expect(subject.transitioning?).to be_falsey end end @@ -4496,6 +4531,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev .to change { subject.merge_status } .from(merge_status.to_s) .to(expected_merge_status) + expect(subject.transitioning?).to be_falsey end end diff --git a/spec/models/performance_monitoring/prometheus_metric_spec.rb b/spec/models/performance_monitoring/prometheus_metric_spec.rb deleted file mode 100644 index 58bb59793cf..00000000000 --- a/spec/models/performance_monitoring/prometheus_metric_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe PerformanceMonitoring::PrometheusMetric do - let(:json_content) do - { - "id" => "metric_of_ages", - "unit" => "count", - "label" => "Metric of Ages", - "query_range" => "http_requests_total" - } - end - - describe '.from_json' do - subject { described_class.from_json(json_content) } - - it 'creates a PrometheusMetric object' do - expect(subject).to be_a described_class - expect(subject.id).to eq(json_content['id']) - expect(subject.unit).to eq(json_content['unit']) - expect(subject.label).to eq(json_content['label']) - expect(subject.query_range).to eq(json_content['query_range']) - end - - describe 'validations' do - context 'json_content is not a hash' do - let(:json_content) { nil } - - subject { described_class.from_json(json_content) } - - it { expect { subject }.to raise_error(ActiveModel::ValidationError) } - end - - context 'when unit is missing' do - before do - json_content['unit'] = nil - end - - subject { described_class.from_json(json_content) } - - it { expect { subject }.to raise_error(ActiveModel::ValidationError) } - end - - context 'when query and query_range is missing' do - before do - json_content['query_range'] = nil - end - - subject { described_class.from_json(json_content) } - - it { expect { subject }.to raise_error(ActiveModel::ValidationError) } - end - - context 'when query_range is missing but query is available' do - before do - json_content['query_range'] = nil - json_content['query'] = 'http_requests_total' - end - - subject { described_class.from_json(json_content) } - - it { is_expected.to be_valid } - end - end - end -end diff --git a/spec/models/performance_monitoring/prometheus_panel_spec.rb b/spec/models/performance_monitoring/prometheus_panel_spec.rb deleted file mode 100644 index 3dc05576826..00000000000 --- a/spec/models/performance_monitoring/prometheus_panel_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe PerformanceMonitoring::PrometheusPanel do - let(:json_content) do - { - "max_value" => 1, - "type" => "area-chart", - "title" => "Chart Title", - "y_label" => "Y-Axis", - "weight" => 1, - "metrics" => [{ - "id" => "metric_of_ages", - "unit" => "count", - "label" => "Metric of Ages", - "query_range" => "http_requests_total" - }] - } - end - - describe '#new' do - it 'accepts old schema format' do - expect { described_class.new(json_content) }.not_to raise_error - end - - it 'accepts new schema format' do - expect { described_class.new(json_content.merge("y_axis" => { "precision" => 0 })) }.not_to raise_error - end - end - - describe '.from_json' do - describe 'validations' do - context 'json_content is not a hash' do - let(:json_content) { nil } - - subject { described_class.from_json(json_content) } - - it { expect { subject }.to raise_error(ActiveModel::ValidationError) } - end - - context 'when title is missing' do - before do - json_content['title'] = nil - end - - subject { described_class.from_json(json_content) } - - it { expect { subject }.to raise_error(ActiveModel::ValidationError) } - end - - context 'when metrics are missing' do - before do - json_content.delete('metrics') - end - - subject { described_class.from_json(json_content) } - - it { expect { subject }.to raise_error(ActiveModel::ValidationError) } - end - end - end - - describe '.id' do - it 'returns hexdigest of group_title, type and title as the panel id' do - group_title = 'Business Group' - panel_type = 'area-chart' - panel_title = 'New feature requests made' - - expect(Digest::SHA2).to receive(:hexdigest).with("#{group_title}#{panel_type}#{panel_title}").and_return('hexdigest') - expect(described_class.new(title: panel_title, type: panel_type).id(group_title)).to eql 'hexdigest' - end - end -end diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb index 756fcd8b7cd..ab2ebf134d7 100644 --- a/spec/requests/api/graphql/ci/jobs_spec.rb +++ b/spec/requests/api/graphql/ci/jobs_spec.rb @@ -139,7 +139,10 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati let(:pipeline) do pipeline = create(:ci_pipeline, project: project, user: user) stage = create(:ci_stage, project: project, pipeline: pipeline, name: 'first', position: 1) - create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'my test job', scheduling_type: :stage) + create( + :ci_build, pipeline: pipeline, name: 'my test job', + scheduling_type: :stage, stage_id: stage.id, stage_idx: stage.position + ) pipeline end @@ -180,10 +183,10 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati previousStageJobsOrNeeds { nodes { ... on CiBuildNeed { - #{all_graphql_fields_for('CiBuildNeed')} + name } ... on CiJob { - #{all_graphql_fields_for('CiJob', excluded: %w[aiFailureAnalysis])} + name } } } @@ -211,10 +214,12 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati before do build_stage = create(:ci_stage, position: 2, name: 'build', project: project, pipeline: pipeline) test_stage = create(:ci_stage, position: 3, name: 'test', project: project, pipeline: pipeline) + deploy_stage = create(:ci_stage, position: 4, name: 'deploy', project: project, pipeline: pipeline) create(:ci_build, pipeline: pipeline, name: 'docker 1 2', scheduling_type: :stage, ci_stage: build_stage, stage_idx: build_stage.position) create(:ci_build, pipeline: pipeline, name: 'docker 2 2', ci_stage: build_stage, stage_idx: build_stage.position, scheduling_type: :dag) create(:ci_build, pipeline: pipeline, name: 'rspec 1 2', scheduling_type: :stage, ci_stage: test_stage, stage_idx: test_stage.position) + create(:ci_build, pipeline: pipeline, name: 'deploy', scheduling_type: :stage, ci_stage: deploy_stage, stage_idx: deploy_stage.position) test_job = create(:ci_build, pipeline: pipeline, name: 'rspec 2 2', scheduling_type: :dag, ci_stage: test_stage, stage_idx: test_stage.position) create(:ci_build_need, build: test_job, name: 'my test job') @@ -255,6 +260,14 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati 'previousStageJobsOrNeeds' => { 'nodes' => [ a_hash_including('name' => 'my test job') ] } + ), + a_hash_including( + 'name' => 'deploy', + 'needs' => { 'nodes' => [] }, + 'previousStageJobsOrNeeds' => { 'nodes' => [ + a_hash_including('name' => 'rspec 1 2'), + a_hash_including('name' => 'rspec 2 2') + ] } ) ) end @@ -613,3 +626,87 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati end end end + +RSpec.describe 'previousStageJobs', feature_category: :pipeline_composition do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :public) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + + let(:query) do + <<~QUERY + { + project(fullPath: "#{project.full_path}") { + pipeline(iid: "#{pipeline.iid}") { + stages { + nodes { + groups { + nodes { + jobs { + nodes { + name + previousStageJobs { + nodes { + name + downstreamPipeline { + id + } + } + } + } + } + } + } + } + } + } + } + } + QUERY + end + + it 'does not produce N+1 queries', :request_store, :use_sql_query_cache do + user1 = create(:user) + user2 = create(:user) + + create_stage_with_build_and_bridge('build', 0) + create_stage_with_build_and_bridge('test', 1) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: user1) + end + + expect(graphql_data_previous_stage_jobs).to eq( + 'build_build' => [], + 'test_build' => %w[build_build] + ) + + create_stage_with_build_and_bridge('deploy', 2) + + expect do + post_graphql(query, current_user: user2) + end.to issue_same_number_of_queries_as(control) + + expect(graphql_data_previous_stage_jobs).to eq( + 'build_build' => [], + 'test_build' => %w[build_build], + 'deploy_build' => %w[test_build] + ) + end + + def create_stage_with_build_and_bridge(stage_name, stage_position) + stage = create(:ci_stage, position: stage_position, name: "#{stage_name}_stage", project: project, pipeline: pipeline) + + create(:ci_build, pipeline: pipeline, name: "#{stage_name}_build", ci_stage: stage, stage_idx: stage.position) + end + + def graphql_data_previous_stage_jobs + stages = graphql_data.dig('project', 'pipeline', 'stages', 'nodes') + groups = stages.flat_map { |stage| stage.dig('groups', 'nodes') } + jobs = groups.flat_map { |group| group.dig('jobs', 'nodes') } + + jobs.each_with_object({}) do |job, previous_stage_jobs| + previous_stage_jobs[job['name']] = job.dig('previousStageJobs', 'nodes').pluck('name') + end + end +end -- cgit v1.2.3