diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-08 12:09:37 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-08 12:09:37 +0000 |
commit | 96897f83e965318f70032eea0196c4c0b807b1d4 (patch) | |
tree | 80cd64c9ad08215adffdc3be89497ad1fdab690e | |
parent | 5bdbc604c8a08f827c3833e2c28ec0c299bb41fc (diff) | |
download | gitlab-ce-96897f83e965318f70032eea0196c4c0b807b1d4.tar.gz |
Add latest changes from gitlab-org/gitlab@master
54 files changed, 701 insertions, 244 deletions
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index a15e22d4742..e6a1a1ba73c 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -1,9 +1,8 @@ <script> -import $ from 'jquery'; import { mapActions } from 'vuex'; import { __, sprintf } from '~/locale'; +import { GlModal } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; -import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import ListItem from './list_item.vue'; @@ -11,7 +10,7 @@ export default { components: { Icon, ListItem, - GlModal: DeprecatedModal2, + GlModal, }, directives: { tooltip, @@ -58,7 +57,7 @@ export default { methods: { ...mapActions(['stageAllChanges', 'unstageAllChanges', 'discardAllChanges']), openDiscardModal() { - $('#discard-all-changes').modal('show'); + this.$refs.discardAllModal.show(); }, unstageAndDiscardAllChanges() { this.unstageAllChanges(); @@ -114,11 +113,12 @@ export default { </p> <gl-modal v-if="!stagedList" - id="discard-all-changes" - :footer-primary-button-text="__('Discard all changes')" - :header-title-text="__('Discard all changes?')" - footer-primary-button-variant="danger" - @submit="unstageAndDiscardAllChanges" + ref="discardAllModal" + ok-variant="danger" + modal-id="discard-all-changes" + :ok-title="__('Discard all changes')" + :title="__('Discard all changes?')" + @ok="unstageAndDiscardAllChanges" > {{ $options.discardModalText }} </gl-modal> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 998d3f2d8ea..36c8b18e205 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -3,6 +3,7 @@ import Vue from 'vue'; import { mapActions, mapGetters, mapState } from 'vuex'; import { GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui'; import { __ } from '~/locale'; +import { modalTypes } from '../constants'; import FindFile from '~/vue_shared/components/file_finder/index.vue'; import NewModal from './new_dropdown/modal.vue'; import IdeSidebar from './ide_side_bar.vue'; @@ -67,7 +68,7 @@ export default { document.querySelector('.navbar-gitlab').classList.add(`theme-${this.themeName}`); }, methods: { - ...mapActions(['toggleFileFinder', 'openNewEntryModal']), + ...mapActions(['toggleFileFinder']), onBeforeUnload(e = {}) { const returnValue = __('Are you sure you want to lose unsaved changes?'); @@ -81,6 +82,9 @@ export default { openFile(file) { this.$router.push(`/project${file.url}`); }, + createNewFile() { + this.$refs.newModal.open(modalTypes.blob); + }, }, }; </script> @@ -137,7 +141,7 @@ export default { variant="success" :title="__('New file')" :aria-label="__('New file')" - @click="openNewEntryModal({ type: 'blob' })" + @click="createNewFile()" > {{ __('New file') }} </gl-deprecated-button> @@ -159,6 +163,6 @@ export default { <component :is="rightPaneComponent" v-if="currentProjectId" /> </div> <ide-status-bar /> - <new-modal /> + <new-modal ref="newModal" /> </article> </template> diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue index 598f3a1dac6..d78cddc0953 100644 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -1,14 +1,17 @@ <script> import { mapState, mapGetters, mapActions } from 'vuex'; +import { modalTypes } from '../constants'; import IdeTreeList from './ide_tree_list.vue'; import Upload from './new_dropdown/upload.vue'; import NewEntryButton from './new_dropdown/button.vue'; +import NewModal from './new_dropdown/modal.vue'; export default { components: { Upload, IdeTreeList, NewEntryButton, + NewModal, }, computed: { ...mapState(['currentBranchId']), @@ -26,7 +29,13 @@ export default { } }, methods: { - ...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry', 'resetOpenFiles']), + ...mapActions(['updateViewer', 'createTempEntry', 'resetOpenFiles']), + createNewFile() { + this.$refs.newModal.open(modalTypes.blob); + }, + createNewFolder() { + this.$refs.newModal.open(modalTypes.tree); + }, }, }; </script> @@ -41,7 +50,7 @@ export default { :show-label="false" class="d-flex border-0 p-0 mr-3 qa-new-file" icon="doc-new" - @click="openNewEntryModal({ type: 'blob' })" + @click="createNewFile()" /> <upload :show-label="false" @@ -54,9 +63,10 @@ export default { :show-label="false" class="d-flex border-0 p-0" icon="folder-new" - @click="openNewEntryModal({ type: 'tree' })" + @click="createNewFolder()" /> </div> + <new-modal ref="newModal" /> </template> </ide-tree-list> </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index 9961c0df52e..9ad17d73716 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -4,12 +4,14 @@ import icon from '~/vue_shared/components/icon.vue'; import upload from './upload.vue'; import ItemButton from './button.vue'; import { modalTypes } from '../../constants'; +import NewModal from '../new_dropdown/modal.vue'; export default { components: { icon, upload, ItemButton, + NewModal, }, props: { type: { @@ -37,9 +39,9 @@ export default { }, }, methods: { - ...mapActions(['createTempEntry', 'openNewEntryModal', 'deleteEntry']), + ...mapActions(['createTempEntry', 'deleteEntry']), createNewItem(type) { - this.openNewEntryModal({ type, path: this.path }); + this.$refs.newModal.open(type, this.path); this.$emit('toggle', false); }, openDropdown() { @@ -109,5 +111,6 @@ export default { </li> </ul> </div> + <new-modal ref="newModal" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index bf3d736ddf3..f9a27d77498 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -1,61 +1,60 @@ <script> -import $ from 'jquery'; import { mapActions, mapState, mapGetters } from 'vuex'; import flash from '~/flash'; import { __, sprintf, s__ } from '~/locale'; -import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; +import { GlModal } from '@gitlab/ui'; import { modalTypes } from '../../constants'; export default { components: { - GlModal: DeprecatedModal2, + GlModal, }, data() { return { name: '', + type: modalTypes.blob, + path: '', }; }, computed: { - ...mapState(['entries', 'entryModal']), + ...mapState(['entries']), ...mapGetters('fileTemplates', ['templateTypes']), entryName: { get() { - const entryPath = this.entryModal.entry.path; - - if (this.entryModal.type === modalTypes.rename) { - return this.name || entryPath; + if (this.type === modalTypes.rename) { + return this.name || this.path; } - return this.name || (entryPath ? `${entryPath}/` : ''); + return this.name || (this.path ? `${this.path}/` : ''); }, set(val) { this.name = val.trim(); }, }, modalTitle() { - if (this.entryModal.type === modalTypes.tree) { + const entry = this.entries[this.path]; + + if (this.type === modalTypes.tree) { return __('Create new directory'); - } else if (this.entryModal.type === modalTypes.rename) { - return this.entryModal.entry.type === modalTypes.tree - ? __('Rename folder') - : __('Rename file'); + } else if (this.type === modalTypes.rename) { + return entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file'); } return __('Create new file'); }, buttonLabel() { - if (this.entryModal.type === modalTypes.tree) { + const entry = this.entries[this.path]; + + if (this.type === modalTypes.tree) { return __('Create directory'); - } else if (this.entryModal.type === modalTypes.rename) { - return this.entryModal.entry.type === modalTypes.tree - ? __('Rename folder') - : __('Rename file'); + } else if (this.type === modalTypes.rename) { + return entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file'); } return __('Create file'); }, isCreatingNewFile() { - return this.entryModal.type === 'blob'; + return this.type === modalTypes.blob; }, placeholder() { return this.isCreatingNewFile ? 'dir/file_name' : 'dir/'; @@ -64,7 +63,7 @@ export default { methods: { ...mapActions(['createTempEntry', 'renameEntry']), submitForm() { - if (this.entryModal.type === modalTypes.rename) { + if (this.type === modalTypes.rename) { if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) { flash( sprintf(s__('The name "%{name}" is already taken in this directory.'), { @@ -82,7 +81,7 @@ export default { parentPath = parentPath.join('/'); this.renameEntry({ - path: this.entryModal.entry.path, + path: this.path, name: entryName, parentPath, }); @@ -90,17 +89,17 @@ export default { } else { this.createTempEntry({ name: this.name, - type: this.entryModal.type, + type: this.type, }); } }, createFromTemplate(template) { this.createTempEntry({ name: template.name, - type: this.entryModal.type, + type: this.type, }); - $('#ide-new-entry').modal('toggle'); + this.$refs.modal.toggle(); }, focusInput() { const name = this.entries[this.entryName] ? this.entries[this.entryName].name : null; @@ -112,8 +111,23 @@ export default { this.$refs.fieldName.setSelectionRange(inputValue.indexOf(name), inputValue.length); } }, - closedModal() { + resetData() { this.name = ''; + this.path = ''; + this.type = modalTypes.blob; + }, + open(type = modalTypes.blob, path = '') { + this.type = type; + this.path = path; + this.$refs.modal.show(); + + // wait for modal to show first + this.$nextTick(() => { + this.focusInput(); + }); + }, + close() { + this.$refs.modal.hide(); }, }, }; @@ -121,15 +135,15 @@ export default { <template> <gl-modal - id="ide-new-entry" - class="qa-new-file-modal" - :header-title-text="modalTitle" - :footer-primary-button-text="buttonLabel" - footer-primary-button-variant="success" - modal-size="lg" - @submit="submitForm" - @open="focusInput" - @closed="closedModal" + ref="modal" + modal-id="ide-new-entry" + modal-class="qa-new-file-modal" + :title="modalTitle" + :ok-title="buttonLabel" + ok-variant="success" + size="lg" + @ok="submitForm" + @hide="resetData" > <div class="form-group row"> <label class="label-bold col-form-label col-sm-2"> {{ __('Name') }} </label> diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index fa2672aaece..ae8550cba76 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -78,6 +78,7 @@ export const commitItemIconMap = { export const modalTypes = { rename: 'rename', tree: 'tree', + blob: 'blob', }; export const commitActionTypes = { diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index 1856a84359f..89422c8a526 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -1,4 +1,3 @@ -import $ from 'jquery'; import Vue from 'vue'; import { escape } from 'lodash'; import { __, sprintf } from '~/locale'; @@ -176,13 +175,6 @@ export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links); export const setErrorMessage = ({ commit }, errorMessage) => commit(types.SET_ERROR_MESSAGE, errorMessage); -export const openNewEntryModal = ({ commit }, { type, path = '' }) => { - commit(types.OPEN_NEW_ENTRY_MODAL, { type, path }); - - // open the modal manually so we don't mess around with dropdown/rows - $('#ide-new-entry').modal('show'); -}; - export const deleteEntry = ({ commit, dispatch, state }, path) => { const entry = state.entries[path]; const { prevPath, prevName, prevParentPath } = entry; diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 78831bdf022..d088d6d5cba 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -73,7 +73,6 @@ export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; -export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL'; export const DELETE_ENTRY = 'DELETE_ENTRY'; export const RENAME_ENTRY = 'RENAME_ENTRY'; export const REVERT_RENAME_ENTRY = 'REVERT_RENAME_ENTRY'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 5d567d9b169..8372c4c4de0 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -192,15 +192,6 @@ export default { [types.SET_ERROR_MESSAGE](state, errorMessage) { Object.assign(state, { errorMessage }); }, - [types.OPEN_NEW_ENTRY_MODAL](state, { type, path }) { - Object.assign(state, { - entryModal: { - type, - path, - entry: { ...state.entries[path] }, - }, - }); - }, [types.DELETE_ENTRY](state, path) { const entry = state.entries[path]; const { tempFile = false } = entry; diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index f2f0a0eac7b..dbd374f1e1c 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -198,3 +198,21 @@ export const OPERATORS = { equalTo: '==', lessThan: '<', }; + +/** + * Dashboard yml files support custom user-defined variables that + * are rendered as input elements in the monitoring dashboard. + * These values can be edited by the user and are passed on to the + * the backend and eventually to Prometheus API proxy. + * + * As of 13.0, the supported types are: + * simple custom -> dropdown elements + * advanced custom -> dropdown elements + * text -> text input elements + * + * Custom variables have a simple and a advanced variant. + */ +export const VARIABLE_TYPES = { + custom: 'custom', + text: 'text', +}; diff --git a/app/assets/javascripts/monitoring/stores/variable_mapping.js b/app/assets/javascripts/monitoring/stores/variable_mapping.js new file mode 100644 index 00000000000..b8abcac5e09 --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/variable_mapping.js @@ -0,0 +1,125 @@ +import { VARIABLE_TYPES } from '../constants'; + +/** + * This file exclusively deals with parsing user-defined variables + * in dashboard yml file. + * + * As of 13.0, simple custom and advanced custom variables are supported. + * + * In the future iterations, text and query variables will be + * supported + * + */ + +/** + * Utility method to determine if a custom variable is + * simple or not. If its not simple, it is advanced. + * + * @param {Array|Object} customVar Array if simple, object if advanced + * @returns {Boolean} true if simple, false if advanced + */ +const isSimpleCustomVariable = customVar => Array.isArray(customVar); + +/** + * Normalize simple and advanced custom variable options to a standard + * format + * @param {Object} custom variable option + * @returns {Object} normalized custom variable options + */ +const normalizeDropdownOptions = ({ default: defaultOpt = false, text, value }) => ({ + default: defaultOpt, + text, + value, +}); + +/** + * Simple custom variables have an array of values. + * This method parses such variables options to a standard format. + * + * @param {String} opt option from simple custom variable + */ +const parseSimpleDropdownOptions = opt => ({ text: opt, value: opt }); + +/** + * Custom advanced variables are rendered as dropdown elements in the dashboard + * header. This method parses advanced custom variables. + * + * @param {Object} advVariable advance custom variable + * @returns {Object} + */ +const customAdvancedVariableParser = advVariable => { + const options = advVariable?.options?.values ?? []; + return { + type: VARIABLE_TYPES.custom, + label: advVariable.label, + options: options.map(normalizeDropdownOptions), + }; +}; + +/** + * Custom simple variables are rendered as dropdown elements in the dashboard + * header. This method parses simple custom variables. + * + * Simple custom variables do not have labels so its set to null here. + * + * @param {Array} customVariable array of options + * @returns {Object} + */ +const customSimpleVariableParser = simpleVar => { + const options = (simpleVar || []).map(parseSimpleDropdownOptions); + return { + type: VARIABLE_TYPES.custom, + label: null, + options: options.map(normalizeDropdownOptions), + }; +}; + +/** + * This method returns a parser based on the type of the variable. + * Currently, the supported variables are simple custom and + * advanced custom only. In the future, this method will support + * text and query variables. + * + * @param {Array|Object} variable + * @return {Function} parser method + */ +const getVariableParser = variable => { + if (isSimpleCustomVariable(variable)) { + return customSimpleVariableParser; + } else if (variable.type === VARIABLE_TYPES.custom) { + return customAdvancedVariableParser; + } + return () => null; +}; + +/** + * This method parses the templating property in the dashboard yml file. + * The templating property has variables that are rendered as input elements + * for the user to edit. The values from input elements are relayed to + * backend and eventually Prometheus API. + * + * This method currently is not used anywhere. Once the issue + * https://gitlab.com/gitlab-org/gitlab/-/issues/214536 is completed, + * this method will have been used by the monitoring dashboard. + * + * @param {Object} templating templating variables from the dashboard yml file + * @returns {Object} a map of processed templating variables + */ +export const parseTemplatingVariables = ({ variables = {} } = {}) => + Object.entries(variables).reduce((acc, [key, variable]) => { + // get the parser + const parser = getVariableParser(variable); + // parse the variable + const parsedVar = parser(variable); + // for simple custom variable label is null and it should be + // replace with key instead + if (parsedVar) { + acc[key] = { + ...parsedVar, + label: parsedVar.label || key, + }; + } + return acc; + }, {}); + +export default {}; diff --git a/app/assets/stylesheets/page_bundles/themes/_dark.scss b/app/assets/stylesheets/page_bundles/themes/_dark.scss index 30822eb7de4..1d316ca2e3f 100644 --- a/app/assets/stylesheets/page_bundles/themes/_dark.scss +++ b/app/assets/stylesheets/page_bundles/themes/_dark.scss @@ -40,7 +40,7 @@ h1, h2, h3, - h4:not(.modal-title), + h4, h5, h6, code, @@ -80,10 +80,6 @@ background-color: $dropdown-hover-background; } - .modal-body { - color: $gl-text-color; - } - .dropdown-menu-toggle svg, .dropdown-menu-toggle svg:hover, .ide-tree-header:not(.ide-pipeline-header) svg, diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 656a567c0a7..214ff2d61d9 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -115,8 +115,11 @@ module Ci state_machine :status, initial: :created do event :enqueue do - transition [:created, :waiting_for_resource, :preparing, :skipped, :scheduled] => :pending + transition [:created, :manual, :waiting_for_resource, :preparing, :skipped, :scheduled] => :pending transition [:success, :failed, :canceled] => :running + + # this is needed to ensure tests to be covered + transition [:running] => :running end event :request_resource do diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 93bd42f8734..d3325a47053 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -42,8 +42,7 @@ module Ci state_machine :status, initial: :created do event :enqueue do - transition [:created, :waiting_for_resource, :preparing] => :pending - transition [:success, :failed, :canceled, :skipped] => :running + transition any - [:pending] => :pending end event :request_resource do diff --git a/app/services/ci/pipeline_processing/atomic_processing_service.rb b/app/services/ci/pipeline_processing/atomic_processing_service.rb index 2a1bf15b9a3..b01a9d2e3b8 100644 --- a/app/services/ci/pipeline_processing/atomic_processing_service.rb +++ b/app/services/ci/pipeline_processing/atomic_processing_service.rb @@ -95,7 +95,7 @@ module Ci def processable_status(processable) if processable.scheduling_type_dag? # Processable uses DAG, get status of all dependent needs - @collection.status_for_names(processable.aggregated_needs_names.to_a) + @collection.status_for_names(processable.aggregated_needs_names.to_a, dag: true) else # Processable uses Stages, get status of prior stage @collection.status_for_prior_stage_position(processable.stage_idx.to_i) diff --git a/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb b/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb index 42e38a5c80f..2228328882d 100644 --- a/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb +++ b/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb @@ -32,14 +32,14 @@ module Ci # This methods gets composite status of all processables def status_of_all - status_for_array(all_statuses) + status_for_array(all_statuses, dag: false) end # This methods gets composite status for processables with given names - def status_for_names(names) + def status_for_names(names, dag:) name_statuses = all_statuses_by_name.slice(*names) - status_for_array(name_statuses.values) + status_for_array(name_statuses.values, dag: dag) end # This methods gets composite status for processables before given stage @@ -48,7 +48,7 @@ module Ci stage_statuses = all_statuses_grouped_by_stage_position .select { |stage_position, _| stage_position < position } - status_for_array(stage_statuses.values.flatten) + status_for_array(stage_statuses.values.flatten, dag: false) end end @@ -65,7 +65,7 @@ module Ci strong_memoize("status_for_stage_position_#{current_position}") do stage_statuses = all_statuses_grouped_by_stage_position[current_position].to_a - status_for_array(stage_statuses.flatten) + status_for_array(stage_statuses.flatten, dag: false) end end @@ -76,7 +76,14 @@ module Ci private - def status_for_array(statuses) + def status_for_array(statuses, dag:) + # TODO: This is hack to support + # the same exact behaviour for Atomic and Legacy processing + # that DAG is blocked from executing if dependent is not "complete" + if dag && statuses.any? { |status| HasStatus::COMPLETED_STATUSES.exclude?(status[:status]) } + return 'pending' + end + result = Gitlab::Ci::Status::Composite .new(statuses) .status diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb index 4f6ae07be7d..3a01192487d 100644 --- a/app/services/tags/destroy_service.rb +++ b/app/services/tags/destroy_service.rb @@ -18,11 +18,6 @@ module Tags .new(project, current_user, tag: tag_name) .execute - push_data = build_push_data(tag) - EventCreateService.new.push(project, current_user, push_data) - project.execute_hooks(push_data.dup, :tag_push_hooks) - project.execute_services(push_data.dup, :tag_push_hooks) - success('Tag was removed') else error('Failed to remove tag') @@ -38,14 +33,5 @@ module Tags def success(message) super().merge(message: message) end - - def build_push_data(tag) - Gitlab::DataBuilder::Push.build( - project: project, - user: current_user, - oldrev: tag.dereferenced_target.sha, - newrev: Gitlab::Git::BLANK_SHA, - ref: "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}") - end end end diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 4040b1094aa..b50f712922f 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -103,7 +103,7 @@ .input-group %span.input-group-prepend .input-group-text / - = f.text_field :build_coverage_regex, class: 'form-control', placeholder: 'Regular expression' + = f.text_field :build_coverage_regex, class: 'form-control', placeholder: 'Regular expression', data: { qa_selector: 'build_coverage_regex_field' } %span.input-group-append .input-group-text / %p.form-text.text-muted @@ -143,7 +143,7 @@ go test -cover (Go) %code coverage: \d+.\d+% of statements - = f.submit _('Save changes'), class: "btn btn-success" + = f.submit _('Save changes'), class: "btn btn-success", data: { qa_selector: 'save_general_pipelines_changes_button' } %hr diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 1358077f2b2..4e14426a069 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -5,7 +5,7 @@ - expanded = expanded_by_default? - general_expanded = @project.errors.empty? ? expanded : true -%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) } +%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded), data: { qa_selector: 'general_pipelines_settings_content' } } .settings-header %h4 = _("General pipelines") diff --git a/changelogs/unreleased/sk-fix-tag-deletion-duplicate-events.yml b/changelogs/unreleased/sk-fix-tag-deletion-duplicate-events.yml new file mode 100644 index 00000000000..8dc6dcb1317 --- /dev/null +++ b/changelogs/unreleased/sk-fix-tag-deletion-duplicate-events.yml @@ -0,0 +1,5 @@ +--- +title: Fix duplicated activity and events on deletion of tag +merge_request: 28861 +author: Sashi Kumar +type: fixed @@ -252,6 +252,7 @@ module QA autoload :Main, 'qa/page/project/settings/main' autoload :Repository, 'qa/page/project/settings/repository' autoload :CICD, 'qa/page/project/settings/ci_cd' + autoload :GeneralPipelines, 'qa/page/project/settings/general_pipelines' autoload :AutoDevops, 'qa/page/project/settings/auto_devops' autoload :DeployKeys, 'qa/page/project/settings/deploy_keys' autoload :DeployTokens, 'qa/page/project/settings/deploy_tokens' diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb index 46f93fad61e..6d4f05f877a 100644 --- a/qa/qa/page/project/settings/ci_cd.rb +++ b/qa/qa/page/project/settings/ci_cd.rb @@ -11,6 +11,13 @@ module QA element :autodevops_settings_content element :runners_settings_content element :variables_settings_content + element :general_pipelines_settings_content + end + + def expand_general_pipelines(&block) + expand_section(:general_pipelines_settings_content) do + Settings::GeneralPipelines.perform(&block) + end end def expand_runners_settings(&block) diff --git a/qa/qa/page/project/settings/general_pipelines.rb b/qa/qa/page/project/settings/general_pipelines.rb new file mode 100644 index 00000000000..2265281a3f4 --- /dev/null +++ b/qa/qa/page/project/settings/general_pipelines.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Settings + class GeneralPipelines < Page::Base + include Common + + view 'app/views/projects/settings/ci_cd/_form.html.haml' do + element :build_coverage_regex_field + element :save_general_pipelines_changes_button + end + + def configure_coverage_regex(pattern) + fill_element :build_coverage_regex_field, pattern + click_element :save_general_pipelines_changes_button + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb new file mode 100644 index 00000000000..bf20c4a70ea --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module QA + context 'Verify', :docker, :runner do + describe 'Code coverage statistics' do + let(:simplecov) { '\(\d+.\d+\%\) covered' } + let(:executor) { "qa-runner-#{Time.now.to_i}" } + let(:runner) do + Resource::Runner.fabricate_via_api! do |runner| + runner.name = executor + end + end + + let(:merge_request) do + Resource::MergeRequest.fabricate_via_api! do |mr| + mr.project = runner.project + mr.file_name = '.gitlab-ci.yml' + mr.file_content = <<~EOF + test: + tags: + - qa + - e2e + script: + - echo '(66.67%) covered' + EOF + end + end + + before do + Flow::Login.sign_in + end + + after do + runner.remove_via_api! + end + + it 'creates an MR with code coverage statistics' do + runner.project.visit! + configure_code_coverage(simplecov) + merge_request.visit! + + Page::MergeRequest::Show.perform do |mr_widget| + Support::Retrier.retry_until(max_attempts: 5, sleep_interval: 5) do + mr_widget.has_pipeline_status?(/Pipeline #\d+ passed/) + end + expect(mr_widget).to have_content('Coverage 66.67%') + end + end + end + + private + + def configure_code_coverage(coverage_tool_pattern) + Page::Project::Menu.perform(&:go_to_ci_cd_settings) + Page::Project::Settings::CICD.perform do |settings| + settings.expand_general_pipelines do |coverage| + coverage.configure_coverage_regex(coverage_tool_pattern) + end + end + end + end +end diff --git a/spec/frontend/ide/components/new_dropdown/index_spec.js b/spec/frontend/ide/components/new_dropdown/index_spec.js index f4fecb68b64..00781c16609 100644 --- a/spec/frontend/ide/components/new_dropdown/index_spec.js +++ b/spec/frontend/ide/components/new_dropdown/index_spec.js @@ -23,9 +23,9 @@ describe('new dropdown component', () => { tree: [], }; - jest.spyOn(vm, 'openNewEntryModal').mockImplementation(() => {}); - vm.$mount(); + + jest.spyOn(vm.$refs.newModal, 'open').mockImplementation(() => {}); }); afterEach(() => { @@ -43,16 +43,16 @@ describe('new dropdown component', () => { }); describe('createNewItem', () => { - it('sets modalType to blob when new file is clicked', () => { + it('opens modal for a blob when new file is clicked', () => { vm.$el.querySelectorAll('.dropdown-menu button')[0].click(); - expect(vm.openNewEntryModal).toHaveBeenCalledWith({ type: 'blob', path: '' }); + expect(vm.$refs.newModal.open).toHaveBeenCalledWith('blob', ''); }); - it('sets modalType to tree when new directory is clicked', () => { + it('opens modal for a tree when new directory is clicked', () => { vm.$el.querySelectorAll('.dropdown-menu button')[2].click(); - expect(vm.openNewEntryModal).toHaveBeenCalledWith({ type: 'tree', path: '' }); + expect(vm.$refs.newModal.open).toHaveBeenCalledWith('tree', ''); }); }); diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js index 2f10bf787b3..23da4df188b 100644 --- a/spec/frontend/ide/components/new_dropdown/modal_spec.js +++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js @@ -14,55 +14,48 @@ describe('new file modal component', () => { vm.$destroy(); }); - describe.each(['tree', 'blob'])('%s', type => { - beforeEach(() => { + describe.each` + entryType | modalTitle | btnTitle | showsFileTemplates + ${'tree'} | ${'Create new directory'} | ${'Create directory'} | ${false} + ${'blob'} | ${'Create new file'} | ${'Create file'} | ${true} + `('$entryType', ({ entryType, modalTitle, btnTitle, showsFileTemplates }) => { + beforeEach(done => { const store = createStore(); - store.state.entryModal = { - type, - path: '', - entry: { - path: '', - }, - }; vm = createComponentWithStore(Component, store).$mount(); - + vm.open(entryType); vm.name = 'testing'; - }); - it(`sets modal title as ${type}`, () => { - const title = type === 'tree' ? 'directory' : 'file'; + vm.$nextTick(done); + }); - expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`); + afterEach(() => { + vm.close(); }); - it(`sets button label as ${type}`, () => { - const title = type === 'tree' ? 'directory' : 'file'; + it(`sets modal title as ${entryType}`, () => { + expect(document.querySelector('.modal-title').textContent.trim()).toBe(modalTitle); + }); - expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`); + it(`sets button label as ${entryType}`, () => { + expect(document.querySelector('.btn-success').textContent.trim()).toBe(btnTitle); }); - it(`sets form label as ${type}`, () => { - expect(vm.$el.querySelector('.label-bold').textContent.trim()).toBe('Name'); + it(`sets form label as ${entryType}`, () => { + expect(document.querySelector('.label-bold').textContent.trim()).toBe('Name'); }); - it(`${type === 'tree' ? 'does not show' : 'shows'} file templates`, () => { - const templateFilesEl = vm.$el.querySelector('.file-templates'); - if (type === 'tree') { - expect(templateFilesEl).toBeNull(); - } else { - expect(templateFilesEl instanceof Element).toBeTruthy(); - } + it(`shows file templates: ${showsFileTemplates}`, () => { + const templateFilesEl = document.querySelector('.file-templates'); + expect(Boolean(templateFilesEl)).toBe(showsFileTemplates); }); }); describe('rename entry', () => { beforeEach(() => { const store = createStore(); - store.state.entryModal = { - type: 'rename', - path: '', - entry: { + store.state.entries = { + 'test-path': { name: 'test', type: 'blob', path: 'test-path', @@ -72,23 +65,29 @@ describe('new file modal component', () => { vm = createComponentWithStore(Component, store).$mount(); }); - ['tree', 'blob'].forEach(type => { - it(`renders title and button for renaming ${type}`, done => { - const text = type === 'tree' ? 'folder' : 'file'; - - vm.$store.state.entryModal.entry.type = type; + it.each` + entryType | modalTitle | btnTitle + ${'tree'} | ${'Rename folder'} | ${'Rename folder'} + ${'blob'} | ${'Rename file'} | ${'Rename file'} + `( + 'renders title and button for renaming $entryType', + ({ entryType, modalTitle, btnTitle }, done) => { + vm.$store.state.entries['test-path'].type = entryType; + vm.open('rename', 'test-path'); vm.$nextTick(() => { - expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Rename ${text}`); - expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Rename ${text}`); + expect(document.querySelector('.modal-title').textContent.trim()).toBe(modalTitle); + expect(document.querySelector('.btn-success').textContent.trim()).toBe(btnTitle); done(); }); - }); - }); + }, + ); describe('entryName', () => { it('returns entries name', () => { + vm.open('rename', 'test-path'); + expect(vm.entryName).toBe('test-path'); }); @@ -115,15 +114,6 @@ describe('new file modal component', () => { describe('submitForm', () => { it('throws an error when target entry exists', () => { const store = createStore(); - store.state.entryModal = { - type: 'rename', - path: 'test-path/test', - entry: { - name: 'test', - type: 'blob', - path: 'test-path/test', - }, - }; store.state.entries = { 'test-path/test': { name: 'test', @@ -132,6 +122,7 @@ describe('new file modal component', () => { }; vm = createComponentWithStore(Component, store).$mount(); + vm.open('rename', 'test-path/test'); expect(createFlash).not.toHaveBeenCalled(); diff --git a/spec/frontend/ide/stores/mutations_spec.js b/spec/frontend/ide/stores/mutations_spec.js index 5d0fe35a10e..bca355b820a 100644 --- a/spec/frontend/ide/stores/mutations_spec.js +++ b/spec/frontend/ide/stores/mutations_spec.js @@ -339,23 +339,6 @@ describe('Multi-file store mutations', () => { }); }); - describe('OPEN_NEW_ENTRY_MODAL', () => { - it('sets entryModal', () => { - localState.entries.testPath = file(); - - mutations.OPEN_NEW_ENTRY_MODAL(localState, { - type: 'test', - path: 'testPath', - }); - - expect(localState.entryModal).toEqual({ - type: 'test', - path: 'testPath', - entry: localState.entries.testPath, - }); - }); - }); - describe('RENAME_ENTRY', () => { beforeEach(() => { localState.trees = { diff --git a/spec/frontend/monitoring/store/variable_mapping_spec.js b/spec/frontend/monitoring/store/variable_mapping_spec.js new file mode 100644 index 00000000000..5081092a519 --- /dev/null +++ b/spec/frontend/monitoring/store/variable_mapping_spec.js @@ -0,0 +1,150 @@ +import { parseTemplatingVariables } from '~/monitoring/stores/variable_mapping'; + +describe('parseTemplatingVariables', () => { + const generateMockTemplatingData = data => { + const vars = data + ? { + variables: { + ...data, + }, + } + : {}; + return { + dashboard: { + templating: vars, + }, + }; + }; + + const simpleVar = ['value1', 'value2', 'value3']; + const advVar = { + label: 'Advanced Var', + type: 'custom', + options: { + values: [ + { value: 'value1', text: 'Var 1 Option 1' }, + { + value: 'value2', + text: 'Var 1 Option 2', + default: true, + }, + ], + }, + }; + const advVarWithoutOptions = { + type: 'custom', + options: {}, + }; + const advVarWithoutLabel = { + type: 'custom', + options: { + values: [ + { value: 'value1', text: 'Var 1 Option 1' }, + { + value: 'value2', + text: 'Var 1 Option 2', + default: true, + }, + ], + }, + }; + const advVarWithoutType = { + label: 'Variable 2', + options: { + values: [ + { value: 'value1', text: 'Var 1 Option 1' }, + { + value: 'value2', + text: 'Var 1 Option 2', + default: true, + }, + ], + }, + }; + + const responseForSimpleCustomVariable = { + simpleVar: { + label: 'simpleVar', + options: [ + { + default: false, + text: 'value1', + value: 'value1', + }, + { + default: false, + text: 'value2', + value: 'value2', + }, + { + default: false, + text: 'value3', + value: 'value3', + }, + ], + type: 'custom', + }, + }; + + const responseForAdvancedCustomVariableWithoutOptions = { + advVarWithoutOptions: { + label: 'advVarWithoutOptions', + options: [], + type: 'custom', + }, + }; + + const responseForAdvancedCustomVariableWithoutLabel = { + advVarWithoutLabel: { + label: 'advVarWithoutLabel', + options: [ + { + default: false, + text: 'Var 1 Option 1', + value: 'value1', + }, + { + default: true, + text: 'Var 1 Option 2', + value: 'value2', + }, + ], + type: 'custom', + }, + }; + + const responseForAdvancedCustomVariable = { + ...responseForSimpleCustomVariable, + advVar: { + label: 'Advanced Var', + options: [ + { + default: false, + text: 'Var 1 Option 1', + value: 'value1', + }, + { + default: true, + text: 'Var 1 Option 2', + value: 'value2', + }, + ], + type: 'custom', + }, + }; + + it.each` + case | input | expected + ${'Returns empty object for no dashboard input'} | ${{}} | ${{}} + ${'Returns empty object for empty dashboard input'} | ${{ dashboard: {} }} | ${{}} + ${'Returns empty object for empty templating prop'} | ${generateMockTemplatingData()} | ${{}} + ${'Returns empty object for empty variables prop'} | ${generateMockTemplatingData({})} | ${{}} + ${'Returns parsed object for simple variable'} | ${generateMockTemplatingData({ simpleVar })} | ${responseForSimpleCustomVariable} + ${'Returns parsed object for advanced variable without options'} | ${generateMockTemplatingData({ advVarWithoutOptions })} | ${responseForAdvancedCustomVariableWithoutOptions} + ${'Returns parsed object for advanced variable without type'} | ${generateMockTemplatingData({ advVarWithoutType })} | ${{}} + ${'Returns parsed object for advanced variable without label'} | ${generateMockTemplatingData({ advVarWithoutLabel })} | ${responseForAdvancedCustomVariableWithoutLabel} + ${'Returns parsed object for simple and advanced variables'} | ${generateMockTemplatingData({ simpleVar, advVar })} | ${responseForAdvancedCustomVariable} + `('$case', ({ input, expected }) => { + expect(parseTemplatingVariables(input?.dashboard?.templating)).toEqual(expected); + }); +}); diff --git a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js b/spec/frontend/reports/components/grouped_test_reports_app_spec.js index 9d7150d95cd..1a01db391da 100644 --- a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js +++ b/spec/frontend/reports/components/grouped_test_reports_app_spec.js @@ -35,7 +35,7 @@ describe('Grouped Test Reports App', () => { }); it('renders success summary text', done => { - setTimeout(() => { + setImmediate(() => { expect(vm.$el.querySelector('.gl-spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary contained no changed test results out of 11 total tests', @@ -49,7 +49,7 @@ describe('Grouped Test Reports App', () => { 'java ant found no changed test results out of 3 total tests', ); done(); - }, 0); + }); }); }); @@ -62,14 +62,14 @@ describe('Grouped Test Reports App', () => { }); it('renders success summary text', done => { - setTimeout(() => { + setImmediate(() => { expect(vm.$el.querySelector('.gl-spinner')).not.toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary results are being parsed', ); done(); - }, 0); + }); }); }); @@ -82,7 +82,7 @@ describe('Grouped Test Reports App', () => { }); it('renders failed summary text + new badge', done => { - setTimeout(() => { + setImmediate(() => { expect(vm.$el.querySelector('.gl-spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary contained 2 failed out of 11 total tests', @@ -95,7 +95,7 @@ describe('Grouped Test Reports App', () => { 'java ant found no changed test results out of 3 total tests', ); done(); - }, 0); + }); }); }); @@ -108,7 +108,7 @@ describe('Grouped Test Reports App', () => { }); it('renders error summary text + new badge', done => { - setTimeout(() => { + setImmediate(() => { expect(vm.$el.querySelector('.gl-spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary contained 2 errors out of 11 total tests', @@ -121,7 +121,7 @@ describe('Grouped Test Reports App', () => { 'rspec:pg found no changed test results out of 8 total tests', ); done(); - }, 0); + }); }); }); @@ -134,7 +134,7 @@ describe('Grouped Test Reports App', () => { }); it('renders summary text', done => { - setTimeout(() => { + setImmediate(() => { expect(vm.$el.querySelector('.gl-spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary contained 2 failed and 2 fixed test results out of 11 total tests', @@ -147,7 +147,7 @@ describe('Grouped Test Reports App', () => { expect(vm.$el.textContent).toContain('New'); expect(vm.$el.textContent).toContain(' java ant found 1 failed out of 3 total tests'); done(); - }, 0); + }); }); }); @@ -160,7 +160,7 @@ describe('Grouped Test Reports App', () => { }); it('renders summary text', done => { - setTimeout(() => { + setImmediate(() => { expect(vm.$el.querySelector('.gl-spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary contained 4 fixed test results out of 11 total tests', @@ -170,11 +170,11 @@ describe('Grouped Test Reports App', () => { 'rspec:pg found 4 fixed test results out of 8 total tests', ); done(); - }, 0); + }); }); it('renders resolved failures', done => { - setTimeout(() => { + setImmediate(() => { expect(vm.$el.querySelector('.report-block-container').textContent).toContain( resolvedFailures.suites[0].resolved_failures[0].name, ); @@ -183,11 +183,11 @@ describe('Grouped Test Reports App', () => { resolvedFailures.suites[0].resolved_failures[1].name, ); done(); - }, 0); + }); }); it('renders resolved errors', done => { - setTimeout(() => { + setImmediate(() => { expect(vm.$el.querySelector('.report-block-container').textContent).toContain( resolvedFailures.suites[0].resolved_errors[0].name, ); @@ -196,7 +196,7 @@ describe('Grouped Test Reports App', () => { resolvedFailures.suites[0].resolved_errors[1].name, ); done(); - }, 0); + }); }); }); @@ -209,7 +209,7 @@ describe('Grouped Test Reports App', () => { }); it('renders an error status for the report', done => { - setTimeout(() => { + setImmediate(() => { const { name } = failedReport.suites[0]; expect(vm.$el.querySelector('.report-block-list-issue').textContent).toContain( @@ -229,12 +229,12 @@ describe('Grouped Test Reports App', () => { }); it('renders loading summary text with loading icon', done => { - setTimeout(() => { + setImmediate(() => { expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary failed loading results', ); done(); - }, 0); + }); }); }); @@ -252,9 +252,9 @@ describe('Grouped Test Reports App', () => { 'Test summary results are being parsed', ); - setTimeout(() => { + setImmediate(() => { done(); - }, 0); + }); }); }); }); diff --git a/spec/javascripts/reports/components/modal_open_name_spec.js b/spec/frontend/reports/components/modal_open_name_spec.js index ae1fb2bf187..d59f3571c4b 100644 --- a/spec/javascripts/reports/components/modal_open_name_spec.js +++ b/spec/frontend/reports/components/modal_open_name_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { mountComponentWithStore } from 'helpers/vue_mount_component_helper'; import component from '~/reports/components/modal_open_name.vue'; Vue.use(Vuex); @@ -38,7 +38,7 @@ describe('Modal open name', () => { }); it('calls openModal actions when button is clicked', () => { - spyOn(vm, 'openModal'); + jest.spyOn(vm, 'openModal').mockImplementation(() => {}); vm.$el.click(); diff --git a/spec/javascripts/reports/components/modal_spec.js b/spec/frontend/reports/components/modal_spec.js index ff046e64b6e..ff046e64b6e 100644 --- a/spec/javascripts/reports/components/modal_spec.js +++ b/spec/frontend/reports/components/modal_spec.js diff --git a/spec/javascripts/reports/components/summary_row_spec.js b/spec/frontend/reports/components/summary_row_spec.js index a19fbad403c..cb0cc025e80 100644 --- a/spec/javascripts/reports/components/summary_row_spec.js +++ b/spec/frontend/reports/components/summary_row_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; import component from '~/reports/components/summary_row.vue'; describe('Summary row', () => { diff --git a/spec/javascripts/reports/components/test_issue_body_spec.js b/spec/frontend/reports/components/test_issue_body_spec.js index a55719a9d36..ff81020a4eb 100644 --- a/spec/javascripts/reports/components/test_issue_body_spec.js +++ b/spec/frontend/reports/components/test_issue_body_spec.js @@ -26,7 +26,7 @@ describe('Test Issue body', () => { props: commonProps, }); - spyOn(vm, 'openModal'); + jest.spyOn(vm, 'openModal').mockImplementation(() => {}); vm.$el.querySelector('button').click(); diff --git a/spec/javascripts/reports/mock_data/mock_data.js b/spec/frontend/reports/mock_data/mock_data.js index 3caaab2fd79..3caaab2fd79 100644 --- a/spec/javascripts/reports/mock_data/mock_data.js +++ b/spec/frontend/reports/mock_data/mock_data.js diff --git a/spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json b/spec/frontend/reports/mock_data/new_and_fixed_failures_report.json index 6141e5433a6..6141e5433a6 100644 --- a/spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json +++ b/spec/frontend/reports/mock_data/new_and_fixed_failures_report.json diff --git a/spec/javascripts/reports/mock_data/new_errors_report.json b/spec/frontend/reports/mock_data/new_errors_report.json index cebf98fdb63..cebf98fdb63 100644 --- a/spec/javascripts/reports/mock_data/new_errors_report.json +++ b/spec/frontend/reports/mock_data/new_errors_report.json diff --git a/spec/javascripts/reports/mock_data/new_failures_report.json b/spec/frontend/reports/mock_data/new_failures_report.json index 8b9c12c6271..8b9c12c6271 100644 --- a/spec/javascripts/reports/mock_data/new_failures_report.json +++ b/spec/frontend/reports/mock_data/new_failures_report.json diff --git a/spec/javascripts/reports/mock_data/no_failures_report.json b/spec/frontend/reports/mock_data/no_failures_report.json index 7da9e0c6211..7da9e0c6211 100644 --- a/spec/javascripts/reports/mock_data/no_failures_report.json +++ b/spec/frontend/reports/mock_data/no_failures_report.json diff --git a/spec/javascripts/reports/mock_data/resolved_failures.json b/spec/frontend/reports/mock_data/resolved_failures.json index 49de6aa840b..49de6aa840b 100644 --- a/spec/javascripts/reports/mock_data/resolved_failures.json +++ b/spec/frontend/reports/mock_data/resolved_failures.json diff --git a/spec/javascripts/reports/store/actions_spec.js b/spec/frontend/reports/store/actions_spec.js index 18fdb179597..3f189736922 100644 --- a/spec/javascripts/reports/store/actions_spec.js +++ b/spec/frontend/reports/store/actions_spec.js @@ -1,6 +1,6 @@ import MockAdapter from 'axios-mock-adapter'; -import testAction from 'spec/helpers/vuex_action_helper'; -import { TEST_HOST } from 'spec/test_constants'; +import testAction from 'helpers/vuex_action_helper'; +import { TEST_HOST } from 'helpers/test_constants'; import axios from '~/lib/utils/axios_utils'; import { setEndpoint, diff --git a/spec/javascripts/reports/store/mutations_spec.js b/spec/frontend/reports/store/mutations_spec.js index 9446cd454ab..9446cd454ab 100644 --- a/spec/javascripts/reports/store/mutations_spec.js +++ b/spec/frontend/reports/store/mutations_spec.js diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index d174fac29ce..458dce027a8 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -53,6 +53,29 @@ describe Ci::Pipeline, :mailer do end end + describe '#set_status' do + where(:from_status, :to_status) do + from_status_names = described_class.state_machines[:status].states.map(&:name) + to_status_names = from_status_names - [:created] # we never want to transition into created + + from_status_names.product(to_status_names) + end + + with_them do + it do + pipeline.status = from_status.to_s + + if from_status != to_status + expect(pipeline.set_status(to_status.to_s)) + .to eq(true) + else + expect(pipeline.set_status(to_status.to_s)) + .to eq(false), "loopback transitions are not allowed" + end + end + end + end + describe '.processables' do before do create(:ci_build, name: 'build', pipeline: pipeline) diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index 3aeaa27abce..a1549532559 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' describe Ci::Stage, :models do - let(:stage) { create(:ci_stage_entity) } + let_it_be(:pipeline) { create(:ci_empty_pipeline) } + let(:stage) { create(:ci_stage_entity, pipeline: pipeline, project: pipeline.project) } it_behaves_like 'having unique enum values' @@ -55,6 +56,29 @@ describe Ci::Stage, :models do end end + describe '#set_status' do + where(:from_status, :to_status) do + from_status_names = described_class.state_machines[:status].states.map(&:name) + to_status_names = from_status_names - [:created] # we never want to transition into created + + from_status_names.product(to_status_names) + end + + with_them do + it do + stage.status = from_status.to_s + + if from_status != to_status + expect(stage.set_status(to_status.to_s)) + .to eq(true) + else + expect(stage.set_status(to_status.to_s)) + .to eq(false), "loopback transitions are not allowed" + end + end + end + end + describe '#update_status' do context 'when stage objects needs to be updated' do before do diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb index b487730d07f..de3c7713ac8 100644 --- a/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb +++ b/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb @@ -18,7 +18,7 @@ describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection do it 'does update existing status of processable' do collection.set_processable_status(test_a.id, 'success', 100) - expect(collection.status_for_names(['test-a'])).to eq('success') + expect(collection.status_for_names(['test-a'], dag: false)).to eq('success') end it 'ignores a missing processable' do @@ -33,15 +33,18 @@ describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection do end describe '#status_for_names' do - where(:names, :status) do - %w[build-a] | 'success' - %w[build-a build-b] | 'failed' - %w[build-a test-a] | 'running' + where(:names, :status, :dag) do + %w[build-a] | 'success' | false + %w[build-a build-b] | 'failed' | false + %w[build-a test-a] | 'running' | false + %w[build-a] | 'success' | true + %w[build-a build-b] | 'failed' | true + %w[build-a test-a] | 'pending' | true end with_them do it 'returns composite status of given names' do - expect(collection.status_for_names(names)).to eq(status) + expect(collection.status_for_names(names, dag: dag)).to eq(status) end end end diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb index 13448ea759b..3b66ecff196 100644 --- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb +++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb @@ -2,17 +2,19 @@ require 'spec_helper' require_relative 'shared_processing_service.rb' -# require_relative 'shared_processing_service_tests_with_yaml.rb' +require_relative 'shared_processing_service_tests_with_yaml.rb' describe Ci::PipelineProcessing::AtomicProcessingService do before do stub_feature_flags(ci_atomic_processing: true) + + # This feature flag is implicit + # Atomic Processing does not process statuses differently + stub_feature_flags(ci_composite_status: true) end it_behaves_like 'Pipeline Processing Service' - # TODO: This needs to be enabled. There is a different behavior when using `needs` depending on - # a `manual` job. More info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29405#note_327520605 - # it_behaves_like 'Pipeline Processing Service Tests With Yaml' + it_behaves_like 'Pipeline Processing Service Tests With Yaml' private diff --git a/spec/services/ci/pipeline_processing/legacy_processing_service_spec.rb b/spec/services/ci/pipeline_processing/legacy_processing_service_spec.rb index d4fb03cf643..fd491bf461b 100644 --- a/spec/services/ci/pipeline_processing/legacy_processing_service_spec.rb +++ b/spec/services/ci/pipeline_processing/legacy_processing_service_spec.rb @@ -7,11 +7,25 @@ require_relative 'shared_processing_service_tests_with_yaml.rb' describe Ci::PipelineProcessing::LegacyProcessingService do before do stub_feature_flags(ci_atomic_processing: false) - stub_feature_flags(ci_composite_status: false) end - it_behaves_like 'Pipeline Processing Service' - it_behaves_like 'Pipeline Processing Service Tests With Yaml' + context 'when ci_composite_status is enabled' do + before do + stub_feature_flags(ci_composite_status: true) + end + + it_behaves_like 'Pipeline Processing Service' + it_behaves_like 'Pipeline Processing Service Tests With Yaml' + end + + context 'when ci_composite_status is disabled' do + before do + stub_feature_flags(ci_composite_status: false) + end + + it_behaves_like 'Pipeline Processing Service' + it_behaves_like 'Pipeline Processing Service Tests With Yaml' + end private diff --git a/spec/services/ci/pipeline_processing/shared_processing_service.rb b/spec/services/ci/pipeline_processing/shared_processing_service.rb index ffe5eacfc48..29fa43001ae 100644 --- a/spec/services/ci/pipeline_processing/shared_processing_service.rb +++ b/spec/services/ci/pipeline_processing/shared_processing_service.rb @@ -816,10 +816,10 @@ shared_examples 'Pipeline Processing Service' do context 'when a needed job is skipped', :sidekiq_inline do let!(:linux_build) { create_build('linux:build', stage: 'build', stage_idx: 0) } let!(:linux_rspec) { create_build('linux:rspec', stage: 'test', stage_idx: 1) } - let!(:deploy) do - create_build('deploy', stage: 'deploy', stage_idx: 2, scheduling_type: :dag, needs: [ - create(:ci_build_need, name: 'linux:rspec') - ]) + let!(:deploy) { create_build('deploy', stage: 'deploy', stage_idx: 2, scheduling_type: :dag) } + + before do + create(:ci_build_need, build: deploy, name: 'linux:build') end it 'skips the jobs depending on it' do @@ -836,6 +836,23 @@ shared_examples 'Pipeline Processing Service' do end end + context 'when a needed job is manual', :sidekiq_inline do + let!(:linux_build) { create_build('linux:build', stage: 'build', stage_idx: 0, when: 'manual', allow_failure: true) } + let!(:deploy) { create_build('deploy', stage: 'deploy', stage_idx: 1, scheduling_type: :dag) } + + before do + create(:ci_build_need, build: deploy, name: 'linux:build') + end + + it 'makes deploy DAG to be waiting for optional manual to finish' do + expect(process_pipeline).to be_truthy + + expect(stages).to eq(%w(skipped created)) + expect(all_builds.manual).to contain_exactly(linux_build) + expect(all_builds.created).to contain_exactly(deploy) + end + end + private def all_builds diff --git a/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb b/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb index ec3c10b3bb5..93f83f0ea3b 100644 --- a/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb +++ b/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb @@ -18,36 +18,40 @@ shared_context 'Pipeline Processing Service Tests With Yaml' do project.add_developer(user) end - it 'follows transitions', :sidekiq_inline do + it 'follows transitions' do expect(pipeline).to be_persisted - check_expectation(test_file.dig('init', 'expect')) + Sidekiq::Worker.drain_all # ensure that all async jobs are executed + check_expectation(test_file.dig('init', 'expect'), "init") - test_file['transitions'].each do |transition| + test_file['transitions'].each_with_index do |transition, idx| event_on_jobs(transition['event'], transition['jobs']) - check_expectation(transition['expect']) + Sidekiq::Worker.drain_all # ensure that all async jobs are executed + check_expectation(transition['expect'], "transition:#{idx}") end end private - def check_expectation(expectation) - expectation.each do |key, value| - case key - when 'pipeline' - expect(pipeline.reload.status).to eq(value) - when 'stages' - expect(pipeline.stages.pluck(:name, :status).to_h).to eq(value) - when 'jobs' - expect(pipeline.builds.latest.pluck(:name, :status).to_h).to eq(value) - end - end + def check_expectation(expectation, message) + expect(current_state.deep_stringify_keys).to eq(expectation), message + end + + def current_state + # reload pipeline and all relations + pipeline.reload + + { + pipeline: pipeline.status, + stages: pipeline.ordered_stages.pluck(:name, :status).to_h, + jobs: pipeline.statuses.latest.pluck(:name, :status).to_h + } end def event_on_jobs(event, job_names) - builds = pipeline.builds.latest.where(name: job_names).to_a - expect(builds.count).to eq(job_names.count) # ensure that we have the same counts + statuses = pipeline.statuses.latest.by_name(job_names).to_a + expect(statuses.count).to eq(job_names.count) # ensure that we have the same counts - builds.each { |build| build.public_send("#{event}!") } + statuses.each { |status| status.public_send("#{event}!") } end end end diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_false.yml b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_false.yml index 1a1412f2a11..1d61cd24f8c 100644 --- a/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_false.yml +++ b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_false.yml @@ -24,9 +24,9 @@ transitions: - event: enqueue jobs: [test] expect: - pipeline: manual + pipeline: pending stages: - test: manual + test: pending deploy: created jobs: test: pending diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true.yml b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true.yml index 39545617926..d8ca563b141 100644 --- a/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true.yml +++ b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true.yml @@ -26,7 +26,7 @@ transitions: expect: pipeline: pending stages: - test: running + test: pending deploy: created jobs: test: pending diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_deploy_on_failure.yml b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_deploy_on_failure.yml index 0c6a5a7e364..d375c6a49e0 100644 --- a/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_deploy_on_failure.yml +++ b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_deploy_on_failure.yml @@ -27,7 +27,7 @@ transitions: expect: pipeline: pending stages: - test: running + test: pending deploy: created jobs: test: pending diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_false.yml b/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_false.yml index d37288b2075..2ffa35b56d7 100644 --- a/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_false.yml +++ b/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_false.yml @@ -23,9 +23,9 @@ transitions: - event: enqueue jobs: [test] expect: - pipeline: manual + pipeline: pending stages: - test: manual + test: pending deploy: created jobs: test: pending diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true.yml b/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true.yml index bac339cc58d..088fab5ca09 100644 --- a/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true.yml +++ b/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true.yml @@ -36,7 +36,7 @@ transitions: expect: pipeline: running stages: - test: running + test: pending deploy: success jobs: test: pending diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true_deploy_on_failure.yml b/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true_deploy_on_failure.yml index ab1b210848a..2b30316aef6 100644 --- a/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true_deploy_on_failure.yml +++ b/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true_deploy_on_failure.yml @@ -26,7 +26,7 @@ transitions: expect: pipeline: pending stages: - test: running + test: pending deploy: skipped jobs: test: pending |