diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-17 12:09:20 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-17 12:09:20 +0000 |
commit | b84eeb256c4a780d902faee1f99ca9a711b3214a (patch) | |
tree | 32918aadbea9210eace50efbce9afbfb8cd3ba84 | |
parent | 53ae6b7e3f83591ad251a3f771f5bf3b8cf087ba (diff) | |
download | gitlab-ce-b84eeb256c4a780d902faee1f99ca9a711b3214a.tar.gz |
Add latest changes from gitlab-org/gitlab@master
91 files changed, 1801 insertions, 200 deletions
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 7171f94decc..babdfb2ed75 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -200,7 +200,7 @@ .use-pg9: services: - - name: postgres:9.6 + - name: postgres:9.6.17 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - name: redis:alpine variables: @@ -209,7 +209,7 @@ .use-pg10: image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34" services: - - name: postgres:10.9 + - name: postgres:10.12 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - name: redis:alpine variables: @@ -217,7 +217,7 @@ .use-pg9-ee: services: - - name: postgres:9.6 + - name: postgres:9.6.17 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - name: redis:alpine - name: elasticsearch:6.4.2 @@ -227,7 +227,7 @@ .use-pg10-ee: image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34" services: - - name: postgres:10.9 + - name: postgres:10.12 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - name: redis:alpine - name: elasticsearch:6.4.2 diff --git a/app/assets/javascripts/issuables_list/components/issuable.vue b/app/assets/javascripts/issuables_list/components/issuable.vue index eb924609a8a..2fd92e009eb 100644 --- a/app/assets/javascripts/issuables_list/components/issuable.vue +++ b/app/assets/javascripts/issuables_list/components/issuable.vue @@ -3,7 +3,7 @@ * This is tightly coupled to projects/issues/_issue.html.haml, * any changes done to the haml need to be reflected here. */ -import { escape, isNumber } from 'underscore'; +import { escape, isNumber } from 'lodash'; import { GlLink, GlTooltipDirective as GlTooltip } from '@gitlab/ui'; import { dateInWords, @@ -19,8 +19,6 @@ import { mergeUrlParams } from '~/lib/utils/url_utility'; import Icon from '~/vue_shared/components/icon.vue'; import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; -const ISSUE_TOKEN = '#'; - export default { components: { Icon, @@ -119,8 +117,7 @@ export default { ); }, referencePath() { - // TODO: The API should return the reference path (it doesn't now) https://gitlab.com/gitlab-org/gitlab/issues/31301 - return `${ISSUE_TOKEN}${this.issuable.iid}`; + return this.issuable.references.relative; }, updatedDateString() { return formatDate(new Date(this.issuable.updated_at), 'mmm d, yyyy h:MMtt'); @@ -230,7 +227,7 @@ export default { </div> <div class="issuable-info"> - <span>{{ referencePath }}</span> + <span class="js-ref-path">{{ referencePath }}</span> <span class="d-none d-sm-inline-block mr-1"> · diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue index 82601363aa4..88d174f96ed 100644 --- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue +++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue @@ -62,9 +62,21 @@ export default { return ( report.existing_failures.length > 0 || report.new_failures.length > 0 || - report.resolved_failures.length > 0 + report.resolved_failures.length > 0 || + report.existing_errors.length > 0 || + report.new_errors.length > 0 || + report.resolved_errors.length > 0 ); }, + unresolvedIssues(report) { + return report.existing_failures.concat(report.existing_errors); + }, + newIssues(report) { + return report.new_failures.concat(report.new_errors); + }, + resolvedIssues(report) { + return report.resolved_failures.concat(report.resolved_errors); + }, }, }; </script> @@ -87,9 +99,9 @@ export default { <issues-list v-if="shouldRenderIssuesList(report)" :key="`issues-list-${i}`" - :unresolved-issues="report.existing_failures" - :new-issues="report.new_failures" - :resolved-issues="report.resolved_failures" + :unresolved-issues="unresolvedIssues(report)" + :new-issues="newIssues(report)" + :resolved-issues="resolvedIssues(report)" :component="$options.componentNames.TestIssueBody" class="report-block-group-list" /> diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js index 2a37f5b74fa..68f6de3a7ee 100644 --- a/app/assets/javascripts/reports/store/mutations.js +++ b/app/assets/javascripts/reports/store/mutations.js @@ -16,6 +16,7 @@ export default { state.summary.total = response.summary.total; state.summary.resolved = response.summary.resolved; state.summary.failed = response.summary.failed; + state.summary.errored = response.summary.errored; state.status = response.status; state.reports = response.suites; @@ -29,6 +30,7 @@ export default { total: 0, resolved: 0, failed: 0, + errored: 0, }; state.status = null; }, diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js index d0b2d0a37f5..4f9eb53e787 100644 --- a/app/assets/javascripts/reports/store/state.js +++ b/app/assets/javascripts/reports/store/state.js @@ -13,6 +13,7 @@ export default () => ({ total: 0, resolved: 0, failed: 0, + errored: 0, }, /** @@ -23,10 +24,14 @@ export default () => ({ * total: {Number}, * resolved: {Number}, * failed: {Number}, + * errored: {Number}, * }, * new_failures: {Array.<Object>}, * resolved_failures: {Array.<Object>}, * existing_failures: {Array.<Object>}, + * new_errors: {Array.<Object>}, + * resolved_errors: {Array.<Object>}, + * existing_errors: {Array.<Object>}, * } */ reports: [], diff --git a/app/assets/javascripts/reports/store/utils.js b/app/assets/javascripts/reports/store/utils.js index 7381f038eaf..ce3ffaae703 100644 --- a/app/assets/javascripts/reports/store/utils.js +++ b/app/assets/javascripts/reports/store/utils.js @@ -8,10 +8,11 @@ import { } from '../constants'; const textBuilder = results => { - const { failed, resolved, total } = results; + const { failed, errored, resolved, total } = results; - const failedString = failed - ? n__('%d failed/error test result', '%d failed/error test results', failed) + const failedOrErrored = (failed || 0) + (errored || 0); + const failedString = failedOrErrored + ? n__('%d failed/error test result', '%d failed/error test results', failedOrErrored) : null; const resolvedString = resolved ? n__('%d fixed test result', '%d fixed test results', resolved) @@ -20,7 +21,7 @@ const textBuilder = results => { let resultsString = s__('Reports|no changed test results'); - if (failed) { + if (failedOrErrored) { if (resolved) { resultsString = sprintf(s__('Reports|%{failedString} and %{resolvedString}'), { failedString, diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index ed2c39e3fd3..5e14339bb07 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -86,8 +86,12 @@ module Boards head(:forbidden) unless can?(current_user, :admin_issue, board) end + def serializer_options(issues) + {} + end + def render_issues(issues, metadata) - data = { issues: serialize_as_json(issues) } + data = { issues: serialize_as_json(issues, opts: serializer_options(issues)) } data.merge!(metadata) render json: data @@ -133,8 +137,10 @@ module Boards IssueSerializer.new(current_user: current_user) end - def serialize_as_json(resource) - serializer.represent(resource, serializer: 'board', include_full_project_path: board.group_board?) + def serialize_as_json(resource, opts: {}) + opts.merge!(include_full_project_path: board.group_board?, serializer: 'board') + + serializer.represent(resource, opts) end def whitelist_query_limiting diff --git a/app/graphql/mutations/notes/update.rb b/app/graphql/mutations/notes/update.rb deleted file mode 100644 index ebf57b800c0..00000000000 --- a/app/graphql/mutations/notes/update.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module Mutations - module Notes - class Update < Base - graphql_name 'UpdateNote' - - authorize :admin_note - - argument :id, - GraphQL::ID_TYPE, - required: true, - description: 'The global id of the note to update' - - argument :body, - GraphQL::STRING_TYPE, - required: true, - description: copy_field_description(Types::Notes::NoteType, :body) - - def resolve(args) - note = authorized_find!(id: args[:id]) - - check_object_is_note!(note) - - note = ::Notes::UpdateService.new( - note.project, - current_user, - { note: args[:body] } - ).execute(note) - - { - note: note.reset, - errors: errors_on_object(note) - } - end - end - end -end diff --git a/app/graphql/mutations/notes/update/base.rb b/app/graphql/mutations/notes/update/base.rb new file mode 100644 index 00000000000..9a53337f253 --- /dev/null +++ b/app/graphql/mutations/notes/update/base.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Mutations + module Notes + module Update + # This is a Base class for the Note update mutations and is not + # mounted as a GraphQL mutation itself. + class Base < Mutations::Notes::Base + authorize :admin_note + + argument :id, + GraphQL::ID_TYPE, + required: true, + description: 'The global id of the note to update' + + def resolve(args) + note = authorized_find!(id: args[:id]) + + pre_update_checks!(note, args) + + updated_note = ::Notes::UpdateService.new( + note.project, + current_user, + note_params(note, args) + ).execute(note) + + # It's possible for updated_note to be `nil`, in the situation + # where the note is deleted within `Notes::UpdateService` due to + # the body of the note only containing Quick Actions. + { + note: updated_note&.reset, + errors: updated_note ? errors_on_object(updated_note) : [] + } + end + + private + + def pre_update_checks!(_note, _args) + raise NotImplementedError + end + + def note_params(_note, args) + { note: args[:body] }.compact + end + end + end + end +end diff --git a/app/graphql/mutations/notes/update/image_diff_note.rb b/app/graphql/mutations/notes/update/image_diff_note.rb new file mode 100644 index 00000000000..7aad3af1e04 --- /dev/null +++ b/app/graphql/mutations/notes/update/image_diff_note.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Mutations + module Notes + module Update + class ImageDiffNote < Mutations::Notes::Update::Base + graphql_name 'UpdateImageDiffNote' + + argument :body, + GraphQL::STRING_TYPE, + required: false, + description: copy_field_description(Types::Notes::NoteType, :body) + + argument :position, + Types::Notes::UpdateDiffImagePositionInputType, + required: false, + description: copy_field_description(Types::Notes::NoteType, :position) + + def ready?(**args) + # As both arguments are optional, validate here that one of the + # arguments are present. + # + # This may be able to be done using InputUnions in the future + # if this RFC is merged: + # https://github.com/graphql/graphql-spec/blob/master/rfcs/InputUnion.md + if args.values_at(:body, :position).compact.blank? + raise Gitlab::Graphql::Errors::ArgumentError, + 'body or position arguments are required' + end + + super(args) + end + + private + + def pre_update_checks!(note, args) + unless note.is_a?(DiffNote) && note.position.on_image? + raise Gitlab::Graphql::Errors::ResourceNotAvailable, + 'Resource is not an ImageDiffNote' + end + end + + def note_params(note, args) + super(note, args).merge( + position: position_params(note, args) + ).compact + end + + def position_params(note, args) + new_position = args[:position]&.to_h&.compact + return unless new_position + + original_position = note.position.to_h + + Gitlab::Diff::Position.new(original_position.merge(new_position)) + end + end + end + end +end diff --git a/app/graphql/mutations/notes/update/note.rb b/app/graphql/mutations/notes/update/note.rb new file mode 100644 index 00000000000..03a174fc8d9 --- /dev/null +++ b/app/graphql/mutations/notes/update/note.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Mutations + module Notes + module Update + class Note < Mutations::Notes::Update::Base + graphql_name 'UpdateNote' + + argument :body, + GraphQL::STRING_TYPE, + required: true, + description: copy_field_description(Types::Notes::NoteType, :body) + + private + + def pre_update_checks!(note, _args) + check_object_is_note!(note) + end + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index fc0a2a099df..ee0f4dbb05f 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -4,7 +4,7 @@ module Types class MutationType < BaseObject include Gitlab::Graphql::MountMutation - graphql_name "Mutation" + graphql_name 'Mutation' mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove @@ -20,7 +20,14 @@ module Types mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true mount_mutation Mutations::Notes::Create::DiffNote, calls_gitaly: true mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true - mount_mutation Mutations::Notes::Update + mount_mutation Mutations::Notes::Update::Note, + description: 'Updates a Note. If the body of the Note contains only quick actions, ' \ + 'the Note will be destroyed during the update, and no Note will be ' \ + 'returned' + mount_mutation Mutations::Notes::Update::ImageDiffNote, + description: 'Updates a DiffNote on an image (a `Note` where the `position.positionType` is `"image"`). ' \ + 'If the body of the Note contains only quick actions, the Note will be ' \ + 'destroyed during the update, and no Note will be returned' mount_mutation Mutations::Notes::Destroy mount_mutation Mutations::Todos::MarkDone mount_mutation Mutations::Todos::Restore diff --git a/app/graphql/types/notes/diff_position_type.rb b/app/graphql/types/notes/diff_position_type.rb index 654562da0a7..cc00feba2e6 100644 --- a/app/graphql/types/notes/diff_position_type.rb +++ b/app/graphql/types/notes/diff_position_type.rb @@ -29,10 +29,10 @@ module Types # Fields for image positions field :x, GraphQL::INT_TYPE, null: true, - description: 'X position on which the comment was made', + description: 'X position of the note', resolve: -> (position, _args, _ctx) { position.x if position.on_image? } field :y, GraphQL::INT_TYPE, null: true, - description: 'Y position on which the comment was made', + description: 'Y position of the note', resolve: -> (position, _args, _ctx) { position.y if position.on_image? } field :width, GraphQL::INT_TYPE, null: true, description: 'Total width of the image', diff --git a/app/graphql/types/notes/update_diff_image_position_input_type.rb b/app/graphql/types/notes/update_diff_image_position_input_type.rb new file mode 100644 index 00000000000..af99764f9f2 --- /dev/null +++ b/app/graphql/types/notes/update_diff_image_position_input_type.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Types + module Notes + # InputType used for updateImageDiffNote mutation. + # + # rubocop: disable Graphql/AuthorizeTypes + class UpdateDiffImagePositionInputType < BaseInputObject + graphql_name 'UpdateDiffImagePositionInput' + + argument :x, GraphQL::INT_TYPE, + required: false, + description: copy_field_description(Types::Notes::DiffPositionType, :x) + + argument :y, GraphQL::INT_TYPE, + required: false, + description: copy_field_description(Types::Notes::DiffPositionType, :y) + + argument :width, GraphQL::INT_TYPE, + required: false, + description: copy_field_description(Types::Notes::DiffPositionType, :width) + + argument :height, GraphQL::INT_TYPE, + required: false, + description: copy_field_description(Types::Notes::DiffPositionType, :height) + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 4ed99b229b5..023790f7d87 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -720,8 +720,7 @@ module ProjectsHelper end def settings_container_registry_expiration_policy_available?(project) - Feature.enabled?(:registry_retention_policies_settings, project) && - Gitlab.config.registry.enabled && + Gitlab.config.registry.enabled && can?(current_user, :destroy_container_image, project) end end diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb index 95bb52d8f97..773b9fead3a 100644 --- a/app/mailers/emails/pipelines.rb +++ b/app/mailers/emails/pipelines.rb @@ -15,7 +15,13 @@ module Emails def pipeline_mail(pipeline, recipients, status) @project = pipeline.project @pipeline = pipeline - @merge_request = pipeline.all_merge_requests.first + + @merge_request = if pipeline.merge_request? + pipeline.merge_request + else + pipeline.merge_requests_as_head_pipeline.first + end + add_headers # We use bcc here because we don't want to generate these emails for a diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb index dde73b567db..39e8408f794 100644 --- a/app/models/concerns/analytics/cycle_analytics/stage.rb +++ b/app/models/concerns/analytics/cycle_analytics/stage.rb @@ -15,8 +15,8 @@ module Analytics validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom? validates :start_event_identifier, presence: true validates :end_event_identifier, presence: true - validates :start_event_label, presence: true, if: :start_event_label_based? - validates :end_event_label, presence: true, if: :end_event_label_based? + validates :start_event_label_id, presence: true, if: :start_event_label_based? + validates :end_event_label_id, presence: true, if: :end_event_label_based? validate :validate_stage_event_pairs validate :validate_labels @@ -109,8 +109,8 @@ module Analytics end def validate_labels - validate_label_within_group(:start_event_label, start_event_label_id) if start_event_label_id_changed? - validate_label_within_group(:end_event_label, end_event_label_id) if end_event_label_id_changed? + validate_label_within_group(:start_event_label_id, start_event_label_id) if start_event_label_id_changed? + validate_label_within_group(:end_event_label_id, end_event_label_id) if end_event_label_id_changed? end def validate_label_within_group(association_name, label_id) diff --git a/app/serializers/test_reports_comparer_entity.rb b/app/serializers/test_reports_comparer_entity.rb index d7a3dd34fdc..5f8a68338cc 100644 --- a/app/serializers/test_reports_comparer_entity.rb +++ b/app/serializers/test_reports_comparer_entity.rb @@ -7,6 +7,7 @@ class TestReportsComparerEntity < Grape::Entity expose :total_count, as: :total expose :resolved_count, as: :resolved expose :failed_count, as: :failed + expose :error_count, as: :errored end expose :suite_comparers, as: :suites, using: TestSuiteComparerEntity diff --git a/app/serializers/test_suite_comparer_entity.rb b/app/serializers/test_suite_comparer_entity.rb index d402a4d5718..78c243f75b8 100644 --- a/app/serializers/test_suite_comparer_entity.rb +++ b/app/serializers/test_suite_comparer_entity.rb @@ -11,6 +11,7 @@ class TestSuiteComparerEntity < Grape::Entity expose :total_count, as: :total expose :resolved_count, as: :resolved expose :failed_count, as: :failed + expose :error_count, as: :errored end # rubocop: disable CodeReuse/ActiveRecord @@ -28,6 +29,20 @@ class TestSuiteComparerEntity < Grape::Entity max_tests(suite.new_failures, suite.existing_failures)) end + expose :new_errors, using: TestCaseEntity do |suite| + suite.new_errors.take(max_tests) + end + + expose :existing_errors, using: TestCaseEntity do |suite| + suite.existing_errors.take( + max_tests(suite.new_errors)) + end + + expose :resolved_errors, using: TestCaseEntity do |suite| + suite.resolved_errors.take( + max_tests(suite.new_errors, suite.existing_errors)) + end + private def max_tests(*used) diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb index 51860adca77..9e87bebbe4e 100644 --- a/app/services/snippets/create_service.rb +++ b/app/services/snippets/create_service.rb @@ -24,7 +24,9 @@ module Snippets spam_check(snippet, current_user) snippet_saved = snippet.with_transaction_returning_status do - snippet.save + if snippet.save && snippet.store_mentions! + create_repository_for(snippet, current_user) + end end if snippet_saved @@ -36,5 +38,11 @@ module Snippets snippet_error_response(snippet, 400) end end + + private + + def create_repository_for(snippet, user) + snippet.create_repository if Feature.enabled?(:version_snippets, user) + end end end diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 0affd9f0e6f..438d390389c 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -27,7 +27,9 @@ .form-group.row .col-sm-12= f.label :format, class: 'control-label-full-width' .col-sm-12 - = f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: 'form-control' + .select-wrapper + = f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: 'form-control select-control' + = icon('chevron-down') .form-group.row .col-sm-12= f.label :content, class: 'control-label-full-width' diff --git a/changelogs/unreleased/196843-remove-feature-flag.yml b/changelogs/unreleased/196843-remove-feature-flag.yml new file mode 100644 index 00000000000..b544afb4344 --- /dev/null +++ b/changelogs/unreleased/196843-remove-feature-flag.yml @@ -0,0 +1,5 @@ +--- +title: Container Registry tag expiration policy settings +merge_request: 25123 +author: +type: added diff --git a/changelogs/unreleased/34353-graphql-mutation-update-image-diff-note.yml b/changelogs/unreleased/34353-graphql-mutation-update-image-diff-note.yml new file mode 100644 index 00000000000..d4e18d0cdb6 --- /dev/null +++ b/changelogs/unreleased/34353-graphql-mutation-update-image-diff-note.yml @@ -0,0 +1,5 @@ +--- +title: Add updateImageDiffNote mutation +merge_request: 24027 +author: +type: added diff --git a/changelogs/unreleased/36505-nuget-the-search-service.yml b/changelogs/unreleased/36505-nuget-the-search-service.yml new file mode 100644 index 00000000000..5dde4d401a0 --- /dev/null +++ b/changelogs/unreleased/36505-nuget-the-search-service.yml @@ -0,0 +1,5 @@ +--- +title: Add specialized index to packages_packages database table +merge_request: 24182 +author: +type: other diff --git a/changelogs/unreleased/Updated-new-edit-wiki-page-to-equal-ui-elements.yml b/changelogs/unreleased/Updated-new-edit-wiki-page-to-equal-ui-elements.yml new file mode 100644 index 00000000000..d1974b15ecc --- /dev/null +++ b/changelogs/unreleased/Updated-new-edit-wiki-page-to-equal-ui-elements.yml @@ -0,0 +1,5 @@ +--- +title: Updated ui elements in wiki page creation +merge_request: 25197 +author: Marc Schwede +type: other diff --git a/changelogs/unreleased/bvl-correct-thread-count.yml b/changelogs/unreleased/bvl-correct-thread-count.yml new file mode 100644 index 00000000000..7cee02d849c --- /dev/null +++ b/changelogs/unreleased/bvl-correct-thread-count.yml @@ -0,0 +1,5 @@ +--- +title: Fix sidekiq jobs not always getting a database connection when running with low concurrency +merge_request: 25261 +author: +type: fixed diff --git a/changelogs/unreleased/fix-blocked-status.yml b/changelogs/unreleased/fix-blocked-status.yml new file mode 100644 index 00000000000..5470fc06443 --- /dev/null +++ b/changelogs/unreleased/fix-blocked-status.yml @@ -0,0 +1,5 @@ +--- +title: Show blocked status for all blocked issues on issue boards +merge_request: 24631 +author: +type: fixed diff --git a/changelogs/unreleased/fix-wrong-mr-id-in-pipeline-failure-email.yml b/changelogs/unreleased/fix-wrong-mr-id-in-pipeline-failure-email.yml new file mode 100644 index 00000000000..f73ffe39eb5 --- /dev/null +++ b/changelogs/unreleased/fix-wrong-mr-id-in-pipeline-failure-email.yml @@ -0,0 +1,5 @@ +--- +title: Fix wrong MR link is shown on pipeline failure email +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fj-39199-create-snippet-repository.yml b/changelogs/unreleased/fj-39199-create-snippet-repository.yml new file mode 100644 index 00000000000..d5ab2d7f1a4 --- /dev/null +++ b/changelogs/unreleased/fj-39199-create-snippet-repository.yml @@ -0,0 +1,5 @@ +--- +title: Create snippet repository when it's created +merge_request: 22269 +author: +type: added diff --git a/changelogs/unreleased/refactoring-entities-file-final.yml b/changelogs/unreleased/refactoring-entities-file-final.yml new file mode 100644 index 00000000000..80f5d5aca7a --- /dev/null +++ b/changelogs/unreleased/refactoring-entities-file-final.yml @@ -0,0 +1,5 @@ +--- +title: Separate project entity into own class file +merge_request: 25297 +author: Rajendra Kadam +type: added diff --git a/db/migrate/20200207090921_add_nuget_index_to_packages_packages.rb b/db/migrate/20200207090921_add_nuget_index_to_packages_packages.rb new file mode 100644 index 00000000000..1bd9c4dc0d0 --- /dev/null +++ b/db/migrate/20200207090921_add_nuget_index_to_packages_packages.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddNugetIndexToPackagesPackages < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'index_packages_project_id_name_partial_for_nuget' + + disable_ddl_transaction! + + def up + add_concurrent_index :packages_packages, [:project_id, :name], name: INDEX_NAME, where: "name <> 'NuGet.Temporary.Package' AND version is not null AND package_type = 4" + end + + def down + remove_concurrent_index_by_name :packages_packages, INDEX_NAME + end +end diff --git a/db/schema.rb b/db/schema.rb index 7b27b2524ce..8836ace2312 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -3012,6 +3012,7 @@ ActiveRecord::Schema.define(version: 2020_02_13_204737) do t.index ["name"], name: "index_packages_packages_on_name_trigram", opclass: :gin_trgm_ops, using: :gin t.index ["project_id", "created_at"], name: "index_packages_packages_on_project_id_and_created_at" t.index ["project_id", "name", "version", "package_type"], name: "idx_packages_packages_on_project_id_name_version_package_type" + t.index ["project_id", "name"], name: "index_packages_project_id_name_partial_for_nuget", where: "(((name)::text <> 'NuGet.Temporary.Package'::text) AND (version IS NOT NULL) AND (package_type = 4))" t.index ["project_id", "package_type"], name: "index_packages_packages_on_project_id_and_package_type" t.index ["project_id", "version"], name: "index_packages_packages_on_project_id_and_version" end diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index ecf34b14aa0..f698db84982 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -1406,12 +1406,12 @@ input DiffImagePositionInput { width: Int! """ - X position on which the comment was made + X position of the note """ x: Int! """ - Y position on which the comment was made + Y position of the note """ y: Int! } @@ -1475,12 +1475,12 @@ type DiffPosition { width: Int """ - X position on which the comment was made + X position of the note """ x: Int """ - Y position on which the comment was made + Y position of the note """ y: Int } @@ -4660,6 +4660,18 @@ type Mutation { todosMarkAllDone(input: TodosMarkAllDoneInput!): TodosMarkAllDonePayload toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload updateEpic(input: UpdateEpicInput!): UpdateEpicPayload + + """ + Updates a DiffNote on an image (a `Note` where the `position.positionType` is + `"image"`). If the body of the Note contains only quick actions, the Note will + be destroyed during the update, and no Note will be returned + """ + updateImageDiffNote(input: UpdateImageDiffNoteInput!): UpdateImageDiffNotePayload + + """ + Updates a Note. If the body of the Note contains only quick actions, the Note + will be destroyed during the update, and no Note will be returned + """ updateNote(input: UpdateNoteInput!): UpdateNotePayload updateSnippet(input: UpdateSnippetInput!): UpdateSnippetPayload } @@ -7533,6 +7545,28 @@ enum TypeEnum { project } +input UpdateDiffImagePositionInput { + """ + Total height of the image + """ + height: Int + + """ + Total width of the image + """ + width: Int + + """ + X position of the note + """ + x: Int + + """ + Y position of the note + """ + y: Int +} + """ Autogenerated input type of UpdateEpic """ @@ -7619,6 +7653,51 @@ type UpdateEpicPayload { } """ +Autogenerated input type of UpdateImageDiffNote +""" +input UpdateImageDiffNoteInput { + """ + Content of the note + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The global id of the note to update + """ + id: ID! + + """ + The position of this note on a diff + """ + position: UpdateDiffImagePositionInput +} + +""" +Autogenerated return type of UpdateImageDiffNote +""" +type UpdateImageDiffNotePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Reasons why the mutation failed. + """ + errors: [String!]! + + """ + The note after mutation + """ + note: Note +} + +""" Autogenerated input type of UpdateNote """ input UpdateNoteInput { diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 33252993682..38a836acb6b 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -8063,7 +8063,7 @@ }, { "name": "x", - "description": "X position on which the comment was made", + "description": "X position of the note", "args": [ ], @@ -8077,7 +8077,7 @@ }, { "name": "y", - "description": "Y position on which the comment was made", + "description": "Y position of the note", "args": [ ], @@ -19427,8 +19427,35 @@ "deprecationReason": null }, { + "name": "updateImageDiffNote", + "description": "Updates a DiffNote on an image (a `Note` where the `position.positionType` is `\"image\"`). If the body of the Note contains only quick actions, the Note will be destroyed during the update, and no Note will be returned", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateImageDiffNoteInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateImageDiffNotePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "updateNote", - "description": null, + "description": "Updates a Note. If the body of the Note contains only quick actions, the Note will be destroyed during the update, and no Note will be returned", "args": [ { "name": "input", @@ -21640,7 +21667,7 @@ }, { "name": "x", - "description": "X position on which the comment was made", + "description": "X position of the note", "type": { "kind": "NON_NULL", "name": null, @@ -21654,7 +21681,7 @@ }, { "name": "y", - "description": "Y position on which the comment was made", + "description": "Y position of the note", "type": { "kind": "NON_NULL", "name": null, @@ -21817,6 +21844,179 @@ }, { "kind": "OBJECT", + "name": "UpdateImageDiffNotePayload", + "description": "Autogenerated return type of UpdateImageDiffNote", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Reasons why the mutation failed.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "note", + "description": "The note after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Note", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateImageDiffNoteInput", + "description": "Autogenerated input type of UpdateImageDiffNote", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "The global id of the note to update", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "body", + "description": "Content of the note", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "position", + "description": "The position of this note on a diff", + "type": { + "kind": "INPUT_OBJECT", + "name": "UpdateDiffImagePositionInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateDiffImagePositionInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "x", + "description": "X position of the note", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "y", + "description": "Y position of the note", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "width", + "description": "Total width of the image", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "height", + "description": "Total height of the image", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "DestroyNotePayload", "description": "Autogenerated return type of DestroyNote", "fields": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 43e7aef59e8..c1a5882eb25 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -246,8 +246,8 @@ Autogenerated return type of DestroySnippet | `oldPath` | String | Path of the file on the start SHA | | `positionType` | DiffPositionType! | Type of file the position refers to | | `width` | Int | Total width of the image | -| `x` | Int | X position on which the comment was made | -| `y` | Int | Y position on which the comment was made | +| `x` | Int | X position of the note | +| `y` | Int | Y position of the note | ## DiffRefs @@ -1230,6 +1230,16 @@ Autogenerated return type of UpdateEpic | `epic` | Epic | The epic after mutation | | `errors` | String! => Array | Reasons why the mutation failed. | +## UpdateImageDiffNotePayload + +Autogenerated return type of UpdateImageDiffNote + +| Name | Type | Description | +| --- | ---- | ---------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Reasons why the mutation failed. | +| `note` | Note | The note after mutation | + ## UpdateNotePayload Autogenerated return type of UpdateNote diff --git a/doc/user/analytics/productivity_analytics.md b/doc/user/analytics/productivity_analytics.md index 572265b5b09..36dd9b5d6bf 100644 --- a/doc/user/analytics/productivity_analytics.md +++ b/doc/user/analytics/productivity_analytics.md @@ -25,10 +25,7 @@ Productivity Analytics allows GitLab users to: ## Accessing metrics and visualizations -To access the **Productivity Analytics** page: - -1. Go to **Analytics** from the top navigation bar. -1. Select **Productivity Analytics** from the menu. +To access the chart, navigate to a group's sidebar and select **Analytics > Productivity Analytics**. The following metrics and visualizations are available on a project or group level - currently only covering **merged** merge requests: diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index 3bdda338b76..ff15b299cea 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -137,7 +137,7 @@ build: If you want to whitelist specific vulnerabilities, you'll need to: - 1. Set `GIT_STRATEGY: fetch` in your `.gitlab-ci.yml` file by following the instructions described in the + 1. Set [`GIT_STRATEGY: fetch`](../../../ci/yaml/README.md#git-strategy) in your `.gitlab-ci.yml` file by following the instructions described in the [overriding the Container Scanning template](#overriding-the-container-scanning-template) section of this document. 1. Define the whitelisted vulnerabilities in a YAML file named `clair-whitelist.yml` which must use the format described in the [following whitelist example file](https://github.com/arminc/clair-scanner/blob/v12/example-whitelist.yaml). @@ -163,18 +163,19 @@ container_scanning: Container Scanning can be [configured](#overriding-the-container-scanning-template) using environment variables. -| Environment Variable | Description | Default | -| ------ | ------ | ------ | -| `KLAR_TRACE` | Set to true to enable more verbose output from klar. | `"false"` | -| `DOCKER_USER` | Username for accessing a Docker registry requiring authentication. | `$CI_REGISTRY_USER` | -| `DOCKER_PASSWORD` | Password for accessing a Docker registry requiring authentication. | `$CI_REGISTRY_PASSWORD` | -| `CLAIR_OUTPUT` | Severity level threshold. Vulnerabilities with severity level higher than or equal to this threshold will be outputted. Supported levels are `Unknown`, `Negligible`, `Low`, `Medium`, `High`, `Critical` and `Defcon1`. | `Unknown` | -| `REGISTRY_INSECURE` | Allow [Klar](https://github.com/optiopay/klar) to access insecure registries (HTTP only). Should only be set to `true` when testing the image locally. | `"false"` | +| Environment Variable | Description | Default | +| ------ | ------ | ------ | +| `KLAR_TRACE` | Set to true to enable more verbose output from klar. | `"false"` | +| `DOCKER_USER` | Username for accessing a Docker registry requiring authentication. | `$CI_REGISTRY_USER` | +| `DOCKER_PASSWORD` | Password for accessing a Docker registry requiring authentication. | `$CI_REGISTRY_PASSWORD` | +| `CLAIR_OUTPUT` | Severity level threshold. Vulnerabilities with severity level higher than or equal to this threshold will be outputted. Supported levels are `Unknown`, `Negligible`, `Low`, `Medium`, `High`, `Critical` and `Defcon1`. | `Unknown` | +| `REGISTRY_INSECURE` | Allow [Klar](https://github.com/optiopay/klar) to access insecure registries (HTTP only). Should only be set to `true` when testing the image locally. | `"false"` | | `CLAIR_VULNERABILITIES_DB_URL` | This variable is explicitly set in the [services section](https://gitlab.com/gitlab-org/gitlab/blob/30522ca8b901223ac8c32b633d8d67f340b159c1/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml#L17-19) of the `Container-Scanning.gitlab-ci.yml` file and defaults to `clair-vulnerabilities-db`. This value represents the address that the [postgres server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db) is running on and **shouldn't be changed** unless you're running the image locally as described in the [Running the scanning tool](https://gitlab.com/gitlab-org/security-products/analyzers/klar/#running-the-scanning-tool) section of the [GitLab klar analyzer readme](https://gitlab.com/gitlab-org/security-products/analyzers/klar). | `clair-vulnerabilities-db` | -| `CI_APPLICATION_REPOSITORY` | Docker repository URL for the image to be scanned. | `$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG` | -| `CI_APPLICATION_TAG` | Docker respository tag for the image to be scanned. | `$CI_COMMIT_SHA` | -| `CLAIR_DB_IMAGE` | The Docker image name and tag for the [postgres server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db). It can be useful to override this value with a specific version, for example, to provide a consistent set of vulnerabilities for integration testing purposes, or to refer to a locally hosted vulnerabilities database for an on-premise air-gapped installation. | `arminc/clair-db:latest` | -| `CLAIR_DB_IMAGE_TAG` | (**DEPRECATED - use `CLAIR_DB_IMAGE` instead**) The Docker image tag for the [postgres server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db). It can be useful to override this value with a specific version, for example, to provide a consistent set of vulnerabilities for integration testing purposes. | `latest` | +| `CI_APPLICATION_REPOSITORY` | Docker repository URL for the image to be scanned. | `$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG` | +| `CI_APPLICATION_TAG` | Docker respository tag for the image to be scanned. | `$CI_COMMIT_SHA` | +| `CLAIR_DB_IMAGE` | The Docker image name and tag for the [Postgres server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db). It can be useful to override this value with a specific version, for example, to provide a consistent set of vulnerabilities for integration testing purposes, or to refer to a locally hosted vulnerabilities database for an on-premise air-gapped installation. | `arminc/clair-db:latest` | +| `CLAIR_DB_IMAGE_TAG` | (**DEPRECATED - use `CLAIR_DB_IMAGE` instead**) The Docker image tag for the [Postgres server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db). It can be useful to override this value with a specific version, for example, to provide a consistent set of vulnerabilities for integration testing purposes. | `latest` | +| `DOCKERFILE_PATH` | The path to the `Dockerfile` to be used for generating remediations. By default, the scanner will look for a file named `Dockerfile` in the root directory of the project, so this variable should only be configured if your `Dockerfile` is in a non-standard location, such as a subdirectory. See [Solutions for vulnerabilities](#solutions-for-vulnerabilities-auto-remediation) for more details. | `Dockerfile` | ## Security Dashboard @@ -187,6 +188,19 @@ vulnerabilities in your groups, projects and pipelines. Read more about the Once a vulnerability is found, you can interact with it. Read more on how to [interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities). +## Solutions for vulnerabilities (auto-remediation) + +Some vulnerabilities can be fixed by applying the solution that GitLab +automatically generates. + +To enable remediation support, the scanning tool _must_ have access to the `Dockerfile` specified by +the `DOCKERFILE_PATH` environment variable. To ensure that the scanning tool has access to this +file, it's necessary to set [`GIT_STRATEGY: fetch`](../../../ci/yaml/README.md#git-strategy) in +your `.gitlab-ci.yml` file by following the instructions described in this document's +[overriding the Container Scanning template](#overriding-the-container-scanning-template) section. + +Read more about the [solutions for vulnerabilities](../index.md#solutions-for-vulnerabilities-auto-remediation). + ## Vulnerabilities database update For more information about the vulnerabilities database update, check the diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md index a48152c2aab..13ea45816b8 100644 --- a/doc/user/application_security/index.md +++ b/doc/user/application_security/index.md @@ -112,6 +112,7 @@ automatically generates. The following scanners are supported: - [Dependency Scanning](dependency_scanning/index.md): Automatic Patch creation is only available for Node.js projects managed with `yarn`. +- [Container Scanning](container_scanning/index.md) #### Manually applying the suggested patch diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md index 627aeac54d6..58da77697d8 100644 --- a/doc/user/project/issues/design_management.md +++ b/doc/user/project/issues/design_management.md @@ -144,8 +144,10 @@ which you can start a new discussion: ![Starting a new discussion on design](img/adding_note_to_design_1.png) -From GitLab 12.8 on, when you are starting a new discussion, you can adjust the badge's position by -dragging it around the image. +[Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34353) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.8, +you can adjust the badge's position by dragging it around the image. This is useful +for when your design layout has changed between revisions, or if you need to move an +existing badge to add a new one in its place. Different discussions have different badge numbers: diff --git a/lib/api/entities/application_setting.rb b/lib/api/entities/application_setting.rb index 6ca2d1e6da4..e9572a8d430 100644 --- a/lib/api/entities/application_setting.rb +++ b/lib/api/entities/application_setting.rb @@ -34,3 +34,5 @@ module API end end end + +API::Entities::ApplicationSetting.prepend_if_ee('EE::API::Entities::ApplicationSetting') diff --git a/lib/api/entities/board.rb b/lib/api/entities/board.rb index afbf5b4b65b..5bb1cde0fa9 100644 --- a/lib/api/entities/board.rb +++ b/lib/api/entities/board.rb @@ -12,3 +12,5 @@ module API end end end + +API::Entities::Board.prepend_if_ee('EE::API::Entities::Board') diff --git a/lib/api/entities/group.rb b/lib/api/entities/group.rb index 8bcad5bca35..ae5ee4784ed 100644 --- a/lib/api/entities/group.rb +++ b/lib/api/entities/group.rb @@ -34,3 +34,5 @@ module API end end end + +API::Entities::Group.prepend_if_ee('EE::API::Entities::Group', with_descendants: true) diff --git a/lib/api/entities/group_detail.rb b/lib/api/entities/group_detail.rb index 97b98aac585..e03047a6e75 100644 --- a/lib/api/entities/group_detail.rb +++ b/lib/api/entities/group_detail.rb @@ -34,3 +34,5 @@ module API end end end + +API::Entities::GroupDetail.prepend_if_ee('EE::API::Entities::GroupDetail') diff --git a/lib/api/entities/identity.rb b/lib/api/entities/identity.rb index 8969a0256c7..52045b6250a 100644 --- a/lib/api/entities/identity.rb +++ b/lib/api/entities/identity.rb @@ -7,3 +7,5 @@ module API end end end + +API::Entities::Identity.prepend_if_ee('EE::API::Entities::Identity') diff --git a/lib/api/entities/issue.rb b/lib/api/entities/issue.rb index b7eb22b2aba..5f2609cf68b 100644 --- a/lib/api/entities/issue.rb +++ b/lib/api/entities/issue.rb @@ -46,3 +46,5 @@ module API end end end + +API::Entities::Issue.prepend_if_ee('EE::API::Entities::Issue') diff --git a/lib/api/entities/issue_basic.rb b/lib/api/entities/issue_basic.rb index 7e4be35d20b..af92f4124f1 100644 --- a/lib/api/entities/issue_basic.rb +++ b/lib/api/entities/issue_basic.rb @@ -41,3 +41,5 @@ module API end end end + +API::Entities::IssueBasic.prepend_if_ee('EE::API::Entities::IssueBasic', with_descendants: true) diff --git a/lib/api/entities/list.rb b/lib/api/entities/list.rb index e856359efc1..480e722c22c 100644 --- a/lib/api/entities/list.rb +++ b/lib/api/entities/list.rb @@ -9,3 +9,5 @@ module API end end end + +API::Entities::List.prepend_if_ee('EE::API::Entities::List') diff --git a/lib/api/entities/member.rb b/lib/api/entities/member.rb index 558f37d1a56..14e97f41e77 100644 --- a/lib/api/entities/member.rb +++ b/lib/api/entities/member.rb @@ -9,3 +9,5 @@ module API end end end + +API::Entities::Member.prepend_if_ee('EE::API::Entities::Member', with_descendants: true) diff --git a/lib/api/entities/merge_request_basic.rb b/lib/api/entities/merge_request_basic.rb index ce8bfa9e670..8cec2c1a97e 100644 --- a/lib/api/entities/merge_request_basic.rb +++ b/lib/api/entities/merge_request_basic.rb @@ -92,3 +92,5 @@ module API end end end + +API::Entities::MergeRequestBasic.prepend_if_ee('EE::API::Entities::MergeRequestBasic', with_descendants: true) diff --git a/lib/api/entities/namespace.rb b/lib/api/entities/namespace.rb index b21dd29c7b4..a7e06cc3e02 100644 --- a/lib/api/entities/namespace.rb +++ b/lib/api/entities/namespace.rb @@ -13,3 +13,5 @@ module API end end end + +API::Entities::Namespace.prepend_if_ee('EE::API::Entities::Namespace') diff --git a/lib/api/entities.rb b/lib/api/entities/project.rb index 479d662f3f5..6ed2ed34360 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities/project.rb @@ -131,21 +131,4 @@ module API end end -# rubocop: disable Cop/InjectEnterpriseEditionModule -::API::Entities::ApplicationSetting.prepend_if_ee('EE::API::Entities::ApplicationSetting') -::API::Entities::Board.prepend_if_ee('EE::API::Entities::Board') -::API::Entities::Group.prepend_if_ee('EE::API::Entities::Group', with_descendants: true) -::API::Entities::GroupDetail.prepend_if_ee('EE::API::Entities::GroupDetail') -::API::Entities::IssueBasic.prepend_if_ee('EE::API::Entities::IssueBasic', with_descendants: true) -::API::Entities::Issue.prepend_if_ee('EE::API::Entities::Issue') -::API::Entities::List.prepend_if_ee('EE::API::Entities::List') -::API::Entities::MergeRequestBasic.prepend_if_ee('EE::API::Entities::MergeRequestBasic', with_descendants: true) -::API::Entities::Member.prepend_if_ee('EE::API::Entities::Member', with_descendants: true) -::API::Entities::Namespace.prepend_if_ee('EE::API::Entities::Namespace') -::API::Entities::Project.prepend_if_ee('EE::API::Entities::Project', with_descendants: true) -::API::Entities::ProtectedRefAccess.prepend_if_ee('EE::API::Entities::ProtectedRefAccess') -::API::Entities::UserPublic.prepend_if_ee('EE::API::Entities::UserPublic', with_descendants: true) -::API::Entities::Todo.prepend_if_ee('EE::API::Entities::Todo') -::API::Entities::ProtectedBranch.prepend_if_ee('EE::API::Entities::ProtectedBranch') -::API::Entities::Identity.prepend_if_ee('EE::API::Entities::Identity') -::API::Entities::UserWithAdmin.prepend_if_ee('EE::API::Entities::UserWithAdmin', with_descendants: true) +API::Entities::Project.prepend_if_ee('EE::API::Entities::Project', with_descendants: true) diff --git a/lib/api/entities/protected_branch.rb b/lib/api/entities/protected_branch.rb index e41d497c836..80c8a791053 100644 --- a/lib/api/entities/protected_branch.rb +++ b/lib/api/entities/protected_branch.rb @@ -10,3 +10,5 @@ module API end end end + +API::Entities::ProtectedBranch.prepend_if_ee('EE::API::Entities::ProtectedBranch') diff --git a/lib/api/entities/protected_ref_access.rb b/lib/api/entities/protected_ref_access.rb index ab878be45d2..f0185705b06 100644 --- a/lib/api/entities/protected_ref_access.rb +++ b/lib/api/entities/protected_ref_access.rb @@ -10,3 +10,5 @@ module API end end end + +API::Entities::ProtectedRefAccess.prepend_if_ee('EE::API::Entities::ProtectedRefAccess') diff --git a/lib/api/entities/todo.rb b/lib/api/entities/todo.rb index 820d1ceaadd..abfdde89bf1 100644 --- a/lib/api/entities/todo.rb +++ b/lib/api/entities/todo.rb @@ -44,3 +44,5 @@ module API end end end + +API::Entities::Todo.prepend_if_ee('EE::API::Entities::Todo') diff --git a/lib/api/entities/user_public.rb b/lib/api/entities/user_public.rb index 100f73760ca..15e9b905bef 100644 --- a/lib/api/entities/user_public.rb +++ b/lib/api/entities/user_public.rb @@ -17,3 +17,5 @@ module API end end end + +API::Entities::UserPublic.prepend_if_ee('EE::API::Entities::UserPublic', with_descendants: true) diff --git a/lib/api/entities/user_with_admin.rb b/lib/api/entities/user_with_admin.rb index c2f873ae802..d3df12200ff 100644 --- a/lib/api/entities/user_with_admin.rb +++ b/lib/api/entities/user_with_admin.rb @@ -7,3 +7,5 @@ module API end end end + +API::Entities::UserWithAdmin.prepend_if_ee('EE::API::Entities::UserWithAdmin', with_descendants: true) diff --git a/lib/gitlab/ci/parsers/test/junit.rb b/lib/gitlab/ci/parsers/test/junit.rb index 8f8cae0b5f2..133eb16a83e 100644 --- a/lib/gitlab/ci/parsers/test/junit.rb +++ b/lib/gitlab/ci/parsers/test/junit.rb @@ -50,10 +50,7 @@ module Gitlab status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED system_output = data['failure'] elsif data['error'] - # For now, as an MVC, we are grouping error test cases together - # with failed ones. But we will improve this further on - # https://gitlab.com/gitlab-org/gitlab/issues/32046. - status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED + status = ::Gitlab::Ci::Reports::TestCase::STATUS_ERROR system_output = data['error'] else status = ::Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS diff --git a/lib/gitlab/ci/reports/test_reports_comparer.rb b/lib/gitlab/ci/reports/test_reports_comparer.rb index 11810bdc0a8..c6f17f0764f 100644 --- a/lib/gitlab/ci/reports/test_reports_comparer.rb +++ b/lib/gitlab/ci/reports/test_reports_comparer.rb @@ -29,7 +29,7 @@ module Gitlab end end - %w(total_count resolved_count failed_count).each do |method| + %w(total_count resolved_count failed_count error_count).each do |method| define_method(method) do # rubocop: disable CodeReuse/ActiveRecord suite_comparers.sum { |suite| suite.public_send(method) } # rubocop:disable GitlabSecurity/PublicSend diff --git a/lib/gitlab/ci/reports/test_suite_comparer.rb b/lib/gitlab/ci/reports/test_suite_comparer.rb index 9cb7db5934c..a58de43e55e 100644 --- a/lib/gitlab/ci/reports/test_suite_comparer.rb +++ b/lib/gitlab/ci/reports/test_suite_comparer.rb @@ -38,6 +38,30 @@ module Gitlab end end + def new_errors + strong_memoize(:new_errors) do + head_suite.error.reject do |key, _| + base_suite.error.include?(key) + end.values + end + end + + def existing_errors + strong_memoize(:existing_errors) do + head_suite.error.select do |key, _| + base_suite.error.include?(key) + end.values + end + end + + def resolved_errors + strong_memoize(:resolved_errors) do + head_suite.success.select do |key, _| + base_suite.error.include?(key) + end.values + end + end + def total_count head_suite.total_count end @@ -47,12 +71,16 @@ module Gitlab end def resolved_count - resolved_failures.count + resolved_failures.count + resolved_errors.count end def failed_count new_failures.count + existing_failures.count end + + def error_count + new_errors.count + existing_errors.count + end end end end diff --git a/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml index 5c790f3e0ab..2333fb4e947 100644 --- a/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml @@ -27,7 +27,7 @@ license_scanning: refs: - branches variables: - - $GITLAB_FEATURES =~ /\blicense_management\b/ + - $GITLAB_FEATURES =~ /\blicense_scanning\b/ except: variables: - $LICENSE_MANAGEMENT_DISABLED diff --git a/lib/gitlab/runtime.rb b/lib/gitlab/runtime.rb index 3f6361c7276..bf579dd3b77 100644 --- a/lib/gitlab/runtime.rb +++ b/lib/gitlab/runtime.rb @@ -78,12 +78,16 @@ module Gitlab end def max_threads + main_thread = 1 + if puma? - Puma.cli_config.options[:max_threads] + Puma.cli_config.options[:max_threads] + main_thread elsif sidekiq? - Sidekiq.options[:concurrency] + # An extra thread for the poller in Sidekiq Cron: + # https://github.com/ondrejbartas/sidekiq-cron#under-the-hood + Sidekiq.options[:concurrency] + main_thread + 1 else - 1 + main_thread end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b8ed2c62e0c..ecf73002112 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -502,6 +502,9 @@ msgstr "" msgid "'%{level}' is not a valid visibility level" msgstr "" +msgid "'%{name}' stage already exists" +msgstr "" + msgid "'%{source}' is not a import source" msgstr "" @@ -6537,6 +6540,9 @@ msgstr "" msgid "DesignManagement|Could not create new discussion. Please try again." msgstr "" +msgid "DesignManagement|Could not update discussion. Please try again." +msgstr "" + msgid "DesignManagement|Delete" msgstr "" @@ -19338,6 +19344,9 @@ msgstr "" msgid "There was a problem communicating with your device." msgstr "" +msgid "There was a problem refreshing the data, please try again" +msgstr "" + msgid "There was a problem saving your custom stage, please try again" msgstr "" @@ -22394,6 +22403,9 @@ msgstr "" msgid "Your comment could not be updated! Please check your network connection and try again." msgstr "" +msgid "Your custom stage '%{title}' was created" +msgstr "" + msgid "Your dashboard has been copied. You can %{web_ide_link_start}edit it here%{web_ide_link_end}." msgstr "" diff --git a/spec/factories/container_expiration_policies.rb b/spec/factories/container_expiration_policies.rb index 951127a4aa7..41c3a7f8cb9 100644 --- a/spec/factories/container_expiration_policies.rb +++ b/spec/factories/container_expiration_policies.rb @@ -2,7 +2,18 @@ FactoryBot.define do factory :container_expiration_policy, class: 'ContainerExpirationPolicy' do - association :project, factory: [:project, :without_container_expiration_policy] + # Note: because of the project_id primary_key on + # container_expiration_policies, and the create_container_expiration_policy + # callback on Project, we need to build the project first before assigning + # it to a container_expiration_policy. + # + # Also, if you wish to assign an existing project to a + # container_expiration_policy, you will then have to destroy the project's + # container_expiration_policy first. + before(:create) do |container_expiration_policy| + container_expiration_policy.project = build(:project) unless container_expiration_policy.project + end + cadence { '1d' } enabled { true } diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 9be0b308680..ba38e1bb312 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -140,12 +140,6 @@ FactoryBot.define do end end - trait :without_container_expiration_policy do - after(:build) do |project| - project.class.skip_callback(:create, :after, :create_container_expiration_policy, raise: false) - end - end - # Build a custom repository by specifying a hash of `filename => content` in # the transient `files` attribute. Each file will be created in its own # commit, operating against the master branch. So, the following call: diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index d12843d7150..94f57cdda74 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -699,6 +699,137 @@ describe 'Merge request > User sees merge widget', :js do end end + context 'when a new error exists' do + let(:base_reports) do + Gitlab::Ci::Reports::TestReports.new.tap do |reports| + reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + reports.get_suite('junit').add_test_case(create_test_case_java_success) + end + end + + let(:head_reports) do + Gitlab::Ci::Reports::TestReports.new.tap do |reports| + reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + reports.get_suite('junit').add_test_case(create_test_case_java_error) + end + end + + it 'shows test reports summary which includes the new error' do + within(".js-reports-container") do + click_button 'Expand' + + expect(page).to have_content('Test summary contained 1 failed/error test result out of 2 total tests') + within(".js-report-section-container") do + expect(page).to have_content('rspec found no changed test results out of 1 total test') + expect(page).to have_content('junit found 1 failed/error test result out of 1 total test') + expect(page).to have_content('New') + expect(page).to have_content('addTest') + end + end + end + + context 'when user clicks the new error' do + it 'shows the test report detail' do + within(".js-reports-container") do + click_button 'Expand' + + within(".js-report-section-container") do + click_button 'addTest' + + expect(page).to have_content('8.88') + end + end + end + end + end + + context 'when an existing error exists' do + let(:base_reports) do + Gitlab::Ci::Reports::TestReports.new.tap do |reports| + reports.get_suite('rspec').add_test_case(create_test_case_rspec_error) + reports.get_suite('junit').add_test_case(create_test_case_java_success) + end + end + + let(:head_reports) do + Gitlab::Ci::Reports::TestReports.new.tap do |reports| + reports.get_suite('rspec').add_test_case(create_test_case_rspec_error) + reports.get_suite('junit').add_test_case(create_test_case_java_success) + end + end + + it 'shows test reports summary which includes the existing error' do + within(".js-reports-container") do + click_button 'Expand' + + expect(page).to have_content('Test summary contained 1 failed/error test result out of 2 total tests') + within(".js-report-section-container") do + expect(page).to have_content('rspec found 1 failed/error test result out of 1 total test') + expect(page).to have_content('junit found no changed test results out of 1 total test') + expect(page).not_to have_content('New') + expect(page).to have_content('Test#sum when a is 4 and b is 4 returns summary') + end + end + end + + context 'when user clicks the existing error' do + it 'shows test report detail of it' do + within(".js-reports-container") do + click_button 'Expand' + + within(".js-report-section-container") do + click_button 'Test#sum when a is 4 and b is 4 returns summary' + + expect(page).to have_content('4.44') + end + end + end + end + end + + context 'when a resolved error exists' do + let(:base_reports) do + Gitlab::Ci::Reports::TestReports.new.tap do |reports| + reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + reports.get_suite('junit').add_test_case(create_test_case_java_error) + end + end + + let(:head_reports) do + Gitlab::Ci::Reports::TestReports.new.tap do |reports| + reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + reports.get_suite('junit').add_test_case(create_test_case_java_success) + end + end + + it 'shows test reports summary which includes the resolved error' do + within(".js-reports-container") do + click_button 'Expand' + + expect(page).to have_content('Test summary contained 1 fixed test result out of 2 total tests') + within(".js-report-section-container") do + expect(page).to have_content('rspec found no changed test results out of 1 total test') + expect(page).to have_content('junit found 1 fixed test result out of 1 total test') + expect(page).to have_content('addTest') + end + end + end + + context 'when user clicks the resolved error' do + it 'shows test report detail of it' do + within(".js-reports-container") do + click_button 'Expand' + + within(".js-report-section-container") do + click_button 'addTest' + + expect(page).to have_content('5.55') + end + end + end + end + end + context 'properly truncates the report' do let(:base_reports) do Gitlab::Ci::Reports::TestReports.new.tap do |reports| diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb index 4c5bc290402..fc1a85c3efe 100644 --- a/spec/features/projects/settings/registry_settings_spec.rb +++ b/spec/features/projects/settings/registry_settings_spec.rb @@ -10,7 +10,6 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy' before do sign_in(user) stub_container_registry_config(enabled: true) - stub_feature_flags(registry_retention_policies_settings: true) end context 'as owner' do @@ -63,15 +62,4 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy' expect(page).not_to have_selector('#js-registry-policies') end end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(registry_retention_policies_settings: false) - visit project_settings_ci_cd_path(project) - end - - it 'does not exists' do - expect(page).not_to have_selector('#js-registry-policies') - end - end end diff --git a/spec/fixtures/api/schemas/entities/test_reports_comparer.json b/spec/fixtures/api/schemas/entities/test_reports_comparer.json index d7880801c01..03812527f71 100644 --- a/spec/fixtures/api/schemas/entities/test_reports_comparer.json +++ b/spec/fixtures/api/schemas/entities/test_reports_comparer.json @@ -12,11 +12,13 @@ "properties": { "total": { "type": "integer" }, "resolved": { "type": "integer" }, + "errored": { "type": "integer" }, "failed": { "type": "integer" } }, "required": [ "total", "resolved", + "errored", "failed" ] }, diff --git a/spec/fixtures/api/schemas/entities/test_suite_comparer.json b/spec/fixtures/api/schemas/entities/test_suite_comparer.json index d63fea1f0db..ecb331ae013 100644 --- a/spec/fixtures/api/schemas/entities/test_suite_comparer.json +++ b/spec/fixtures/api/schemas/entities/test_suite_comparer.json @@ -16,17 +16,17 @@ "properties": { "total": { "type": "integer" }, "resolved": { "type": "integer" }, + "errored": { "type": "integer" }, "failed": { "type": "integer" } }, - "required": [ - "total", - "resolved", - "failed" - ] + "required": ["total", "resolved", "errored", "failed"] }, "new_failures": { "type": "array", "items": { "$ref": "test_case.json" } }, "resolved_failures": { "type": "array", "items": { "$ref": "test_case.json" } }, - "existing_failures": { "type": "array", "items": { "$ref": "test_case.json" } } + "existing_failures": { "type": "array", "items": { "$ref": "test_case.json" } }, + "new_errors": { "type": "array", "items": { "$ref": "test_case.json" } }, + "resolved_errors": { "type": "array", "items": { "$ref": "test_case.json" } }, + "existing_errors": { "type": "array", "items": { "$ref": "test_case.json" } } }, "additionalProperties": false } diff --git a/spec/frontend/issuables_list/components/issuable_spec.js b/spec/frontend/issuables_list/components/issuable_spec.js index 81f6b60ae25..980def06078 100644 --- a/spec/frontend/issuables_list/components/issuable_spec.js +++ b/spec/frontend/issuables_list/components/issuable_spec.js @@ -122,6 +122,10 @@ describe('Issuable component', () => { expect(finder().exists()).toBe(false); }); + it('show relative reference path', () => { + expect(wrapper.find('.js-ref-path').text()).toBe(issuable.references.relative); + }); + it('does not have closed text', () => { expect(wrapper.text()).not.toContain(TEXT_CLOSED); }); diff --git a/spec/frontend/issuables_list/issuable_list_test_data.js b/spec/frontend/issuables_list/issuable_list_test_data.js index 617780fd736..19d8ee7f71a 100644 --- a/spec/frontend/issuables_list/issuable_list_test_data.js +++ b/spec/frontend/issuables_list/issuable_list_test_data.js @@ -26,6 +26,9 @@ export const simpleIssue = { web_url: 'http://localhost:3001/h5bp/html5-boilerplate/issues/31', has_tasks: false, weight: null, + references: { + relative: 'html-boilerplate#45', + }, }; export const testLabels = [ diff --git a/spec/frontend/reports/store/utils_spec.js b/spec/frontend/reports/store/utils_spec.js index f0141b9e162..0d9a8dd4585 100644 --- a/spec/frontend/reports/store/utils_spec.js +++ b/spec/frontend/reports/store/utils_spec.js @@ -35,6 +35,16 @@ describe('Reports store utils', () => { ); }); + it('should render text for multiple errored results', () => { + const name = 'Test summary'; + const data = { errored: 7, total: 10 }; + const result = utils.summaryTextBuilder(name, data); + + expect(result).toBe( + 'Test summary contained 7 failed/error test results out of 10 total tests', + ); + }); + it('should render text for multiple fixed results', () => { const name = 'Test summary'; const data = { resolved: 4, total: 10 }; @@ -62,6 +72,27 @@ describe('Reports store utils', () => { 'Test summary contained 1 failed/error test result and 1 fixed test result out of 10 total tests', ); }); + + it('should render text for singular failed, errored, and fixed results', () => { + // these will be singular when the copy is updated + const name = 'Test summary'; + const data = { failed: 1, errored: 1, resolved: 1, total: 10 }; + const result = utils.summaryTextBuilder(name, data); + + expect(result).toBe( + 'Test summary contained 2 failed/error test results and 1 fixed test result out of 10 total tests', + ); + }); + + it('should render text for multiple failed, errored, and fixed results', () => { + const name = 'Test summary'; + const data = { failed: 2, errored: 3, resolved: 4, total: 10 }; + const result = utils.summaryTextBuilder(name, data); + + expect(result).toBe( + 'Test summary contained 5 failed/error test results and 4 fixed test results out of 10 total tests', + ); + }); }); describe('reportTextBuilder', () => { @@ -89,6 +120,14 @@ describe('Reports store utils', () => { expect(result).toBe('Rspec found 3 failed/error test results out of 10 total tests'); }); + it('should render text for multiple errored results', () => { + const name = 'Rspec'; + const data = { errored: 7, total: 10 }; + const result = utils.reportTextBuilder(name, data); + + expect(result).toBe('Rspec found 7 failed/error test results out of 10 total tests'); + }); + it('should render text for multiple fixed results', () => { const name = 'Rspec'; const data = { resolved: 4, total: 10 }; @@ -116,6 +155,27 @@ describe('Reports store utils', () => { 'Rspec found 1 failed/error test result and 1 fixed test result out of 10 total tests', ); }); + + it('should render text for singular failed, errored, and fixed results', () => { + // these will be singular when the copy is updated + const name = 'Rspec'; + const data = { failed: 1, errored: 1, resolved: 1, total: 10 }; + const result = utils.reportTextBuilder(name, data); + + expect(result).toBe( + 'Rspec found 2 failed/error test results and 1 fixed test result out of 10 total tests', + ); + }); + + it('should render text for multiple failed, errored, and fixed results', () => { + const name = 'Rspec'; + const data = { failed: 2, errored: 3, resolved: 4, total: 10 }; + const result = utils.reportTextBuilder(name, data); + + expect(result).toBe( + 'Rspec found 5 failed/error test results and 4 fixed test results out of 10 total tests', + ); + }); }); describe('statusIcon', () => { diff --git a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js index 1b006cdbd4e..154aa881d2d 100644 --- a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js +++ b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js @@ -5,6 +5,7 @@ import state from '~/reports/store/state'; import component from '~/reports/components/grouped_test_reports_app.vue'; import mountComponent from '../../helpers/vue_mount_component_helper'; import newFailedTestReports from '../mock_data/new_failures_report.json'; +import newErrorsTestReports from '../mock_data/new_errors_report.json'; import successTestReports from '../mock_data/no_failures_report.json'; import mixedResultsTestReports from '../mock_data/new_and_fixed_failures_report.json'; import resolvedFailures from '../mock_data/resolved_failures.json'; @@ -99,6 +100,34 @@ describe('Grouped Test Reports App', () => { }); }); + describe('with new error result', () => { + beforeEach(() => { + mock.onGet('test_results.json').reply(200, newErrorsTestReports, {}); + vm = mountComponent(Component, { + endpoint: 'test_results.json', + }); + }); + + it('renders error summary text + new badge', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.gl-spinner')).toBeNull(); + expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( + 'Test summary contained 2 failed/error test results out of 11 total tests', + ); + + expect(vm.$el.textContent).toContain( + 'karma found 2 failed/error test results out of 3 total tests', + ); + + expect(vm.$el.textContent).toContain('New'); + expect(vm.$el.textContent).toContain( + 'rspec:pg found no changed test results out of 8 total tests', + ); + done(); + }, 0); + }); + }); + describe('with mixed results', () => { beforeEach(() => { mock.onGet('test_results.json').reply(200, mixedResultsTestReports, {}); @@ -127,7 +156,7 @@ describe('Grouped Test Reports App', () => { }); }); - describe('with resolved failures', () => { + describe('with resolved failures and resolved errors', () => { beforeEach(() => { mock.onGet('test_results.json').reply(200, resolvedFailures, {}); vm = mountComponent(Component, { @@ -139,11 +168,11 @@ describe('Grouped Test Reports App', () => { setTimeout(() => { expect(vm.$el.querySelector('.gl-spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( - 'Test summary contained 2 fixed test results out of 11 total tests', + 'Test summary contained 4 fixed test results out of 11 total tests', ); expect(vm.$el.textContent).toContain( - 'rspec:pg found 2 fixed test results out of 8 total tests', + 'rspec:pg found 4 fixed test results out of 8 total tests', ); done(); }, 0); @@ -161,6 +190,19 @@ describe('Grouped Test Reports App', () => { done(); }, 0); }); + + it('renders resolved errors', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.report-block-container').textContent).toContain( + resolvedFailures.suites[0].resolved_errors[0].name, + ); + + expect(vm.$el.querySelector('.report-block-container').textContent).toContain( + resolvedFailures.suites[0].resolved_errors[1].name, + ); + done(); + }, 0); + }); }); describe('with error', () => { diff --git a/spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json b/spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json index ceaf894375a..6141e5433a6 100644 --- a/spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json +++ b/spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json @@ -1 +1,55 @@ -{"status":"failed","summary":{"total":11,"resolved":2,"failed":2},"suites":[{"name":"rspec:pg","status":"failed","summary":{"total":8,"resolved":2,"failed":1},"new_failures":[{"status":"failed","name":"Test#subtract when a is 2 and b is 1 returns correct result","execution_time":0.00908,"system_output":"Failure/Error: is_expected.to eq(1)\n\n expected: 1\n got: 3\n\n (compared using ==)\n./spec/test_spec.rb:43:in `block (4 levels) in <top (required)>'"}],"resolved_failures":[{"status":"success","name":"Test#sum when a is 1 and b is 2 returns summary","execution_time":0.000318,"system_output":null},{"status":"success","name":"Test#sum when a is 100 and b is 200 returns summary","execution_time":0.000074,"system_output":null}],"existing_failures":[]},{"name":"java ant","status":"failed","summary":{"total":3,"resolved":0,"failed":1},"new_failures":[],"resolved_failures":[],"existing_failures":[{"status":"failed","name":"sumTest","execution_time":0.004,"system_output":"junit.framework.AssertionFailedError: expected:<3> but was:<-1>\n\tat CalculatorTest.sumTest(Unknown Source)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n"}]}]}
\ No newline at end of file +{ + "status": "failed", + "summary": { "total": 11, "resolved": 2, "errored": 0, "failed": 2 }, + "suites": [ + { + "name": "rspec:pg", + "status": "failed", + "summary": { "total": 8, "resolved": 2, "errored": 0, "failed": 1 }, + "new_failures": [ + { + "status": "failed", + "name": "Test#subtract when a is 2 and b is 1 returns correct result", + "execution_time": 0.00908, + "system_output": "Failure/Error: is_expected.to eq(1)\n\n expected: 1\n got: 3\n\n (compared using ==)\n./spec/test_spec.rb:43:in `block (4 levels) in <top (required)>'" + } + ], + "resolved_failures": [ + { + "status": "success", + "name": "Test#sum when a is 1 and b is 2 returns summary", + "execution_time": 0.000318, + "system_output": null + }, + { + "status": "success", + "name": "Test#sum when a is 100 and b is 200 returns summary", + "execution_time": 0.000074, + "system_output": null + } + ], + "existing_failures": [], + "new_errors": [], + "resolved_errors": [], + "existing_errors": [] + }, + { + "name": "java ant", + "status": "failed", + "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 1 }, + "new_failures": [], + "resolved_failures": [], + "existing_failures": [ + { + "status": "failed", + "name": "sumTest", + "execution_time": 0.004, + "system_output": "junit.framework.AssertionFailedError: expected:<3> but was:<-1>\n\tat CalculatorTest.sumTest(Unknown Source)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n" + } + ], + "new_errors": [], + "resolved_errors": [], + "existing_errors": [] + } + ] +} diff --git a/spec/javascripts/reports/mock_data/new_errors_report.json b/spec/javascripts/reports/mock_data/new_errors_report.json new file mode 100644 index 00000000000..cebf98fdb63 --- /dev/null +++ b/spec/javascripts/reports/mock_data/new_errors_report.json @@ -0,0 +1,38 @@ +{ + "summary": { "total": 11, "resolved": 0, "errored": 2, "failed": 0 }, + "suites": [ + { + "name": "rspec:pg", + "summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 0 }, + "new_failures": [], + "resolved_failures": [], + "existing_failures": [], + "new_errors": [], + "resolved_errors": [], + "existing_errors": [] + }, + { + "name": "karma", + "summary": { "total": 3, "resolved": 0, "errored": 2, "failed": 0 }, + "new_failures": [], + "resolved_failures": [], + "existing_failures": [], + "new_errors": [ + { + "result": "error", + "name": "Test#sum when a is 1 and b is 2 returns summary", + "execution_time": 0.009411, + "system_output": "Failed: Error in render: 'TypeError: Cannot read property 'status' of undefined'" + }, + { + "result": "error", + "name": "Test#sum when a is 100 and b is 200 returns summary", + "execution_time": 0.000162, + "system_output": "Failed: Error in render: 'TypeError: Cannot read property 'length' of undefined'" + } + ], + "resolved_errors": [], + "existing_errors": [] + } + ] +} diff --git a/spec/javascripts/reports/mock_data/new_failures_report.json b/spec/javascripts/reports/mock_data/new_failures_report.json index 930efe16f65..8b9c12c6271 100644 --- a/spec/javascripts/reports/mock_data/new_failures_report.json +++ b/spec/javascripts/reports/mock_data/new_failures_report.json @@ -1 +1,38 @@ -{"summary":{"total":11,"resolved":0,"failed":2},"suites":[{"name":"rspec:pg","summary":{"total":8,"resolved":0,"failed":2},"new_failures":[{"result":"failure","name":"Test#sum when a is 1 and b is 2 returns summary","execution_time":0.009411,"system_output":"Failure/Error: is_expected.to eq(3)\n\n expected: 3\n got: -1\n\n (compared using ==)\n./spec/test_spec.rb:12:in `block (4 levels) in <top (required)>'"},{"result":"failure","name":"Test#sum when a is 100 and b is 200 returns summary","execution_time":0.000162,"system_output":"Failure/Error: is_expected.to eq(300)\n\n expected: 300\n got: -100\n\n (compared using ==)\n./spec/test_spec.rb:21:in `block (4 levels) in <top (required)>'"}],"resolved_failures":[],"existing_failures":[]},{"name":"java ant","summary":{"total":3,"resolved":0,"failed":0},"new_failures":[],"resolved_failures":[],"existing_failures":[]}]}
\ No newline at end of file +{ + "summary": { "total": 11, "resolved": 0, "errored": 0, "failed": 2 }, + "suites": [ + { + "name": "rspec:pg", + "summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 2 }, + "new_failures": [ + { + "result": "failure", + "name": "Test#sum when a is 1 and b is 2 returns summary", + "execution_time": 0.009411, + "system_output": "Failure/Error: is_expected.to eq(3)\n\n expected: 3\n got: -1\n\n (compared using ==)\n./spec/test_spec.rb:12:in `block (4 levels) in <top (required)>'" + }, + { + "result": "failure", + "name": "Test#sum when a is 100 and b is 200 returns summary", + "execution_time": 0.000162, + "system_output": "Failure/Error: is_expected.to eq(300)\n\n expected: 300\n got: -100\n\n (compared using ==)\n./spec/test_spec.rb:21:in `block (4 levels) in <top (required)>'" + } + ], + "resolved_failures": [], + "existing_failures": [], + "new_errors": [], + "resolved_errors": [], + "existing_errors": [] + }, + { + "name": "java ant", + "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 0 }, + "new_failures": [], + "resolved_failures": [], + "existing_failures": [], + "new_errors": [], + "resolved_errors": [], + "existing_errors": [] + } + ] +} diff --git a/spec/javascripts/reports/mock_data/no_failures_report.json b/spec/javascripts/reports/mock_data/no_failures_report.json index 6c0675ff7dc..7da9e0c6211 100644 --- a/spec/javascripts/reports/mock_data/no_failures_report.json +++ b/spec/javascripts/reports/mock_data/no_failures_report.json @@ -1 +1,28 @@ -{"status":"success","summary":{"total":11,"resolved":0,"failed":0},"suites":[{"name":"rspec:pg","status":"success","summary":{"total":8,"resolved":0,"failed":0},"new_failures":[],"resolved_failures":[],"existing_failures":[]},{"name":"java ant","status":"success","summary":{"total":3,"resolved":0,"failed":0},"new_failures":[],"resolved_failures":[],"existing_failures":[]}]}
\ No newline at end of file +{ + "status": "success", + "summary": { "total": 11, "resolved": 0, "errored": 0, "failed": 0 }, + "suites": [ + { + "name": "rspec:pg", + "status": "success", + "summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 0 }, + "new_failures": [], + "resolved_failures": [], + "existing_failures": [], + "new_errors": [], + "resolved_errors": [], + "existing_errors": [] + }, + { + "name": "java ant", + "status": "success", + "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 0 }, + "new_failures": [], + "resolved_failures": [], + "existing_failures": [], + "new_errors": [], + "resolved_errors": [], + "existing_errors": [] + } + ] +} diff --git a/spec/javascripts/reports/mock_data/resolved_failures.json b/spec/javascripts/reports/mock_data/resolved_failures.json index d1f347ce5e6..49de6aa840b 100644 --- a/spec/javascripts/reports/mock_data/resolved_failures.json +++ b/spec/javascripts/reports/mock_data/resolved_failures.json @@ -1,11 +1,11 @@ { "status": "success", - "summary": { "total": 11, "resolved": 2, "failed": 0 }, + "summary": { "total": 11, "resolved": 4, "errored": 0, "failed": 0 }, "suites": [ { "name": "rspec:pg", "status": "success", - "summary": { "total": 8, "resolved": 2, "failed": 0 }, + "summary": { "total": 8, "resolved": 4, "errored": 0, "failed": 0 }, "new_failures": [], "resolved_failures": [ { @@ -23,15 +23,36 @@ "stack_trace": null } ], - "existing_failures": [] + "existing_failures": [], + "new_errors": [], + "resolved_errors": [ + { + "status": "success", + "name": "Test#sum when a is 4 and b is 4 returns summary", + "execution_time": 0.00342, + "system_output": null, + "stack_trace": null + }, + { + "status": "success", + "name": "Test#sum when a is 40 and b is 400 returns summary", + "execution_time": 0.0000231, + "system_output": null, + "stack_trace": null + } + ], + "existing_errors": [] }, { "name": "java ant", "status": "success", - "summary": { "total": 3, "resolved": 0, "failed": 0 }, + "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 0 }, "new_failures": [], "resolved_failures": [], - "existing_failures": [] + "existing_failures": [], + "new_errors": [], + "resolved_errors": [], + "existing_errors": [] } ] } diff --git a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb index 6a7fe7a5927..b91cf1dd3ed 100644 --- a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb +++ b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb @@ -99,7 +99,7 @@ describe Gitlab::Ci::Parsers::Test::Junit do let(:testcase_content) { '<error>Some error</error>' } it_behaves_like '<testcase> XML parser', - ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED, + ::Gitlab::Ci::Reports::TestCase::STATUS_ERROR, 'Some error' end diff --git a/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb index 48eef0643b2..d731afe1fff 100644 --- a/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb @@ -57,6 +57,17 @@ describe Gitlab::Ci::Reports::TestReportsComparer do is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED) end end + + context 'when there is an error test case in head suites' do + before do + head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + head_reports.get_suite('junit').add_test_case(create_test_case_java_error) + end + + it 'returns the total status in head suite' do + is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED) + end + end end describe '#total_count' do @@ -75,7 +86,7 @@ describe Gitlab::Ci::Reports::TestReportsComparer do describe '#resolved_count' do subject { comparer.resolved_count } - context 'when there is a resolved test case in head suites' do + context 'when there is a resolved failure test case in head suites' do before do base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) base_reports.get_suite('junit').add_test_case(create_test_case_java_failed) @@ -88,6 +99,19 @@ describe Gitlab::Ci::Reports::TestReportsComparer do end end + context 'when there is a resolved error test case in head suites' do + before do + base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + base_reports.get_suite('junit').add_test_case(create_test_case_java_error) + head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + head_reports.get_suite('junit').add_test_case(create_test_case_java_success) + end + + it 'returns the correct count' do + is_expected.to eq(1) + end + end + context 'when there are no resolved test cases in head suites' do before do base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) @@ -127,4 +151,30 @@ describe Gitlab::Ci::Reports::TestReportsComparer do end end end + + describe '#error_count' do + subject { comparer.error_count } + + context 'when there is an error test case in head suites' do + before do + head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + head_reports.get_suite('junit').add_test_case(create_test_case_java_error) + end + + it 'returns the correct count' do + is_expected.to eq(1) + end + end + + context 'when there are no error test cases in head suites' do + before do + head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + head_reports.get_suite('junit').add_test_case(create_test_case_rspec_success) + end + + it 'returns the correct count' do + is_expected.to eq(0) + end + end + end end diff --git a/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb index cf4690bb334..2d2179a690b 100644 --- a/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb @@ -9,8 +9,9 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do let(:name) { 'rpsec' } let(:base_suite) { Gitlab::Ci::Reports::TestSuite.new(name) } let(:head_suite) { Gitlab::Ci::Reports::TestSuite.new(name) } - let(:test_case_success) { create_test_case_rspec_success } - let(:test_case_failed) { create_test_case_rspec_failed } + let(:test_case_success) { create_test_case_java_success } + let(:test_case_failed) { create_test_case_java_failed } + let(:test_case_error) { create_test_case_java_error } describe '#new_failures' do subject { comparer.new_failures } @@ -135,6 +136,129 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do end end + describe '#new_errors' do + subject { comparer.new_errors } + + context 'when head suite has a new error test case which does not exist in base' do + before do + base_suite.add_test_case(test_case_success) + head_suite.add_test_case(test_case_error) + end + + it 'returns the error test case' do + is_expected.to eq([test_case_error]) + end + end + + context 'when head suite still has an error test case which errored in base' do + before do + base_suite.add_test_case(test_case_error) + head_suite.add_test_case(test_case_error) + end + + it 'does not return the error test case' do + is_expected.to be_empty + end + end + + context 'when head suite has a success test case which errored in base' do + before do + base_suite.add_test_case(test_case_error) + head_suite.add_test_case(test_case_success) + end + + it 'does not return the error test case' do + is_expected.to be_empty + end + end + end + + describe '#existing_errors' do + subject { comparer.existing_errors } + + context 'when head suite has a new error test case which does not exist in base' do + before do + base_suite.add_test_case(test_case_success) + head_suite.add_test_case(test_case_error) + end + + it 'does not return the error test case' do + is_expected.to be_empty + end + end + + context 'when head suite still has an error test case which errored in base' do + before do + base_suite.add_test_case(test_case_error) + head_suite.add_test_case(test_case_error) + end + + it 'returns the error test case' do + is_expected.to eq([test_case_error]) + end + end + + context 'when head suite has a success test case which errored in base' do + before do + base_suite.add_test_case(test_case_error) + head_suite.add_test_case(test_case_success) + end + + it 'does not return the error test case' do + is_expected.to be_empty + end + end + end + + describe '#resolved_errors' do + subject { comparer.resolved_errors } + + context 'when head suite has a new error test case which does not exist in base' do + before do + base_suite.add_test_case(test_case_success) + head_suite.add_test_case(test_case_error) + end + + it 'does not return the error test case' do + is_expected.to be_empty + end + + it 'returns the correct resolved count' do + expect(comparer.resolved_count).to eq(0) + end + end + + context 'when head suite still has an error test case which errored in base' do + before do + base_suite.add_test_case(test_case_error) + head_suite.add_test_case(test_case_error) + end + + it 'does not return the error test case' do + is_expected.to be_empty + end + + it 'returns the correct resolved count' do + expect(comparer.resolved_count).to eq(0) + end + end + + context 'when head suite has a success test case which errored in base' do + before do + base_suite.add_test_case(test_case_error) + head_suite.add_test_case(test_case_success) + end + + it 'returns the resolved test case' do + is_expected.to eq([test_case_success]) + end + + it 'returns the correct resolved count' do + expect(comparer.resolved_count).to eq(1) + end + end + end + describe '#total_count' do subject { comparer.total_count } @@ -208,7 +332,17 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do head_suite.add_test_case(test_case_failed) end - it 'returns the total status in head suite' do + it 'returns the total status in head suite as failed' do + is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED) + end + end + + context 'when there is an error test case in head suite' do + before do + head_suite.add_test_case(test_case_error) + end + + it 'returns the total status in head suite as failed' do is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED) end end diff --git a/spec/lib/gitlab/ci/reports/test_suite_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_spec.rb index 8646db43bc8..217713fd899 100644 --- a/spec/lib/gitlab/ci/reports/test_suite_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_suite_spec.rb @@ -74,6 +74,15 @@ describe Gitlab::Ci::Reports::TestSuite do it { is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED) } end + + context 'when a test case errored' do + before do + test_suite.add_test_case(test_case_success) + test_suite.add_test_case(test_case_error) + end + + it { is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED) } + end end Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type| diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 7250258061a..4dadb310029 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -36,8 +36,6 @@ issues: - vulnerability_links - related_vulnerabilities - user_mentions -- blocked_by_issue_links -- blocked_by_issues events: - author - project diff --git a/spec/lib/gitlab/runtime_spec.rb b/spec/lib/gitlab/runtime_spec.rb index 56df73161b4..34a775fc206 100644 --- a/spec/lib/gitlab/runtime_spec.rb +++ b/spec/lib/gitlab/runtime_spec.rb @@ -50,7 +50,7 @@ describe Gitlab::Runtime do allow(puma_type).to receive_message_chain(:cli_config, :options).and_return(max_threads: 2) end - it_behaves_like "valid runtime", :puma, 2 + it_behaves_like "valid runtime", :puma, 3 end context "unicorn" do @@ -71,7 +71,7 @@ describe Gitlab::Runtime do allow(sidekiq_type).to receive(:options).and_return(concurrency: 2) end - it_behaves_like "valid runtime", :sidekiq, 2 + it_behaves_like "valid runtime", :sidekiq, 4 end context "console" do diff --git a/spec/mailers/emails/pipelines_spec.rb b/spec/mailers/emails/pipelines_spec.rb index 8d4afe9f00f..ad1aa915fbb 100644 --- a/spec/mailers/emails/pipelines_spec.rb +++ b/spec/mailers/emails/pipelines_spec.rb @@ -19,6 +19,25 @@ describe Emails::Pipelines do expect(subject).to have_body_text status_text end + context 'when pipeline on master branch has a merge request' do + let(:pipeline) { create(:ci_pipeline, ref: 'master', sha: sha, project: project) } + + let!(:merge_request) do + create(:merge_request, source_branch: 'master', target_branch: 'feature', + source_project: project, target_project: project) + end + + it 'has correct information that there is no merge request link' do + expect(subject) + .to have_subject "#{project.name} | Pipeline ##{pipeline.id} has " \ + "#{status} for #{pipeline.source_ref} | " \ + "#{pipeline.short_sha}".to_s + + expect(subject).to have_body_text pipeline.source_ref + expect(subject).to have_body_text status_text + end + end + context 'when pipeline for merge requests' do let(:pipeline) { merge_request.all_pipelines.first } @@ -28,7 +47,7 @@ describe Emails::Pipelines do target_project: project) end - it 'has a correct information with merge request link' do + it 'has correct information that there is a merge request link' do expect(subject) .to have_subject "#{project.name} | Pipeline ##{pipeline.id} has " \ "#{status} for #{pipeline.source_ref} | " \ @@ -39,6 +58,27 @@ describe Emails::Pipelines do expect(subject).not_to have_body_text pipeline.ref end end + + context 'when branch pipeline is set to a merge request as a head pipeline' do + let(:pipeline) do + create(:ci_pipeline, project: project, ref: ref, sha: sha, + merge_requests_as_head_pipeline: [merge_request]) + end + + let(:merge_request) do + create(:merge_request, source_project: project, target_project: project) + end + + it 'has correct information that there is a merge request link' do + expect(subject) + .to have_subject "#{project.name} | Pipeline ##{pipeline.id} has " \ + "#{status} for #{pipeline.source_ref} | " \ + "#{pipeline.short_sha} in !#{merge_request.iid}".to_s + + expect(subject).to have_body_text merge_request.to_reference + expect(subject).to have_body_text pipeline.source_ref + end + end end describe '#pipeline_success_email' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index dc055244af7..6c90a1b5614 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -153,6 +153,16 @@ describe Project do expect(project.container_expiration_policy).to be_persisted end + it 'does not create another container expiration policy if there is already one' do + project = build(:project) + + expect do + container_expiration_policy = create(:container_expiration_policy, project: project) + + expect(project.container_expiration_policy).to eq(container_expiration_policy) + end.to change { ContainerExpirationPolicy.count }.by(1) + end + it 'automatically creates a Pages metadata row' do expect(project.pages_metadatum).to be_an_instance_of(ProjectPagesMetadatum) expect(project.pages_metadatum).to be_persisted diff --git a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb new file mode 100644 index 00000000000..0362fef2d2e --- /dev/null +++ b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb @@ -0,0 +1,244 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Updating an image DiffNote' do + include GraphqlHelpers + using RSpec::Parameterized::TableSyntax + + let_it_be(:noteable) { create(:merge_request, :with_diffs) } + let_it_be(:original_body) { 'Original body' } + let_it_be(:original_position) do + Gitlab::Diff::Position.new( + old_path: 'files/images/any_image.png', + new_path: 'files/images/any_image.png', + width: 10, + height: 20, + x: 1, + y: 2, + diff_refs: noteable.diff_refs, + position_type: 'image' + ) + end + let_it_be(:updated_body) { 'Updated body' } + let_it_be(:updated_width) { 50 } + let_it_be(:updated_height) { 100 } + let_it_be(:updated_x) { 5 } + let_it_be(:updated_y) { 10 } + let(:updated_position) do + { + width: updated_width, + height: updated_height, + x: updated_x, + y: updated_y + } + end + let!(:diff_note) do + create(:image_diff_note_on_merge_request, + noteable: noteable, + project: noteable.project, + note: original_body, + position: original_position) + end + let(:mutation) do + variables = { + id: GitlabSchema.id_from_object(diff_note).to_s, + body: updated_body, + position: updated_position + } + + graphql_mutation(:update_image_diff_note, variables) + end + + def mutation_response + graphql_mutation_response(:update_image_diff_note) + end + + context 'when the user does not have permission' do + let_it_be(:current_user) { create(:user) } + + it_behaves_like 'a mutation that returns top-level errors', + errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + + it 'does not update the DiffNote' do + post_graphql_mutation(mutation, current_user: current_user) + + diff_note.reload + + expect(diff_note).to have_attributes( + note: original_body, + position: have_attributes( + width: original_position.width, + height: original_position.height, + x: original_position.x, + y: original_position.y + ) + ) + end + end + + context 'when the user has permission' do + let(:current_user) { diff_note.author } + + it 'updates the DiffNote' do + post_graphql_mutation(mutation, current_user: current_user) + + diff_note.reload + + expect(diff_note).to have_attributes( + note: updated_body, + position: have_attributes( + width: updated_width, + height: updated_height, + x: updated_x, + y: updated_y + ) + ) + end + + it 'returns the updated DiffNote' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['note']).to include( + 'body' => updated_body, + 'position' => hash_including( + 'width' => updated_width, + 'height' => updated_height, + 'x' => updated_x, + 'y' => updated_y + ) + ) + end + + describe 'updating single properties at a time' do + where(:property, :new_value) do + :body | 'foo' + :width | 19 + :height | 18 + :x | 17 + :y | 16 + end + + with_them do + # Properties that will be POSTed: + let(:updated_body) { value(:body) } + let(:updated_width) { value(:width) } + let(:updated_height) { value(:height) } + let(:updated_x) { value(:x) } + let(:updated_y) { value(:y) } + # Expectations of the properties: + let(:expected_body) { value(:body) || original_body } + let(:expected_width) { value(:width) || original_position.width } + let(:expected_height) { value(:height) || original_position.height } + let(:expected_x) { value(:x) || original_position.x } + let(:expected_y) { value(:y) || original_position.y } + + def value(prop) + new_value if property == prop + end + + it 'updates the DiffNote correctly' do + post_graphql_mutation(mutation, current_user: current_user) + + diff_note.reload + + expect(diff_note).to have_attributes( + note: expected_body, + position: have_attributes( + width: expected_width, + height: expected_height, + x: expected_x, + y: expected_y + ) + ) + end + end + + context 'when position is nil' do + let(:updated_position) { nil } + + it 'updates the DiffNote correctly' do + post_graphql_mutation(mutation, current_user: current_user) + + diff_note.reload + + expect(diff_note).to have_attributes( + note: updated_body, + position: original_position + ) + end + end + end + + context 'when both body and position args are blank' do + let(:updated_body) { nil } + let(:updated_position) { nil } + + it_behaves_like 'a mutation that returns top-level errors', errors: ['body or position arguments are required'] + end + + context 'when resource is not a DiffNote on an image' do + let!(:diff_note) { create(:diff_note_on_merge_request, note: original_body) } + + it_behaves_like 'a mutation that returns top-level errors', errors: ['Resource is not an ImageDiffNote'] + end + + context 'when there are ActiveRecord validation errors' do + before do + expect(diff_note).to receive_message_chain( + :errors, + :full_messages + ).and_return(['Error 1', 'Error 2']) + + expect_next_instance_of(Notes::UpdateService) do |service| + expect(service).to receive(:execute).and_return(diff_note) + end + end + + it_behaves_like 'a mutation that returns errors in the response', errors: ['Error 1', 'Error 2'] + + it 'does not update the DiffNote' do + post_graphql_mutation(mutation, current_user: current_user) + + diff_note.reload + + expect(diff_note).to have_attributes( + note: original_body, + position: have_attributes( + width: original_position.width, + height: original_position.height, + x: original_position.x, + y: original_position.y + ) + ) + end + + it 'returns the DiffNote with its original body' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['note']).to include( + 'body' => original_body, + 'position' => hash_including( + 'width' => original_position.width, + 'height' => original_position.height, + 'x' => original_position.x, + 'y' => original_position.y + ) + ) + end + end + + context 'when body only contains quick actions' do + let(:updated_body) { '/close' } + + it 'returns a nil note and empty errors' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to include( + 'errors' => [], + 'note' => nil + ) + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/notes/update_spec.rb b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb index 958f640995a..a5c6b72005e 100644 --- a/spec/requests/api/graphql/mutations/notes/update_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb @@ -22,7 +22,7 @@ describe 'Updating a Note' do end context 'when the user does not have permission' do - let(:current_user) { create(:user) } + let_it_be(:current_user) { create(:user) } it_behaves_like 'a mutation that returns top-level errors', errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] @@ -68,5 +68,18 @@ describe 'Updating a Note' do expect(mutation_response['note']['body']).to eq(original_body) end end + + context 'when body only contains quick actions' do + let(:updated_body) { '/close' } + + it 'returns a nil note and empty errors' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to include( + 'errors' => [], + 'note' => nil + ) + end + end end end diff --git a/spec/serializers/test_reports_comparer_entity_spec.rb b/spec/serializers/test_reports_comparer_entity_spec.rb index 2627ad536e4..e7dabc67325 100644 --- a/spec/serializers/test_reports_comparer_entity_spec.rb +++ b/spec/serializers/test_reports_comparer_entity_spec.rb @@ -24,7 +24,7 @@ describe TestReportsComparerEntity do it 'contains correct compared test reports details' do expect(subject[:status]).to eq('success') - expect(subject[:summary]).to include(total: 2, resolved: 0, failed: 0) + expect(subject[:summary]).to include(total: 2, resolved: 0, failed: 0, errored: 0) expect(subject[:suites].first[:name]).to eq('rspec') expect(subject[:suites].first[:status]).to eq('success') expect(subject[:suites].second[:name]).to eq('junit') @@ -42,7 +42,7 @@ describe TestReportsComparerEntity do it 'contains correct compared test reports details' do expect(subject[:status]).to eq('failed') - expect(subject[:summary]).to include(total: 2, resolved: 0, failed: 1) + expect(subject[:summary]).to include(total: 2, resolved: 0, failed: 1, errored: 0) expect(subject[:suites].first[:name]).to eq('rspec') expect(subject[:suites].first[:status]).to eq('success') expect(subject[:suites].second[:name]).to eq('junit') @@ -60,7 +60,7 @@ describe TestReportsComparerEntity do it 'contains correct compared test reports details' do expect(subject[:status]).to eq('success') - expect(subject[:summary]).to include(total: 2, resolved: 1, failed: 0) + expect(subject[:summary]).to include(total: 2, resolved: 1, failed: 0, errored: 0) expect(subject[:suites].first[:name]).to eq('rspec') expect(subject[:suites].first[:status]).to eq('success') expect(subject[:suites].second[:name]).to eq('junit') diff --git a/spec/serializers/test_suite_comparer_entity_spec.rb b/spec/serializers/test_suite_comparer_entity_spec.rb index e22387130a1..9790777a570 100644 --- a/spec/serializers/test_suite_comparer_entity_spec.rb +++ b/spec/serializers/test_suite_comparer_entity_spec.rb @@ -12,6 +12,7 @@ describe TestSuiteComparerEntity do let(:head_suite) { Gitlab::Ci::Reports::TestSuite.new(name) } let(:test_case_success) { create_test_case_rspec_success } let(:test_case_failed) { create_test_case_rspec_failed } + let(:test_case_error) { create_test_case_rspec_error } describe '#as_json' do subject { entity.as_json } @@ -25,7 +26,7 @@ describe TestSuiteComparerEntity do it 'contains correct compared test suite details' do expect(subject[:name]).to eq(name) expect(subject[:status]).to eq('failed') - expect(subject[:summary]).to include(total: 1, resolved: 0, failed: 1) + expect(subject[:summary]).to include(total: 1, resolved: 0, failed: 1, errored: 0) subject[:new_failures].first.tap do |new_failure| expect(new_failure[:status]).to eq(test_case_failed.status) expect(new_failure[:name]).to eq(test_case_failed.name) @@ -37,6 +38,27 @@ describe TestSuiteComparerEntity do end end + context 'when head suite has a new error test case which does not exist in base' do + before do + base_suite.add_test_case(test_case_success) + head_suite.add_test_case(test_case_error) + end + + it 'contains correct compared test suite details' do + expect(subject[:name]).to eq(name) + expect(subject[:status]).to eq('failed') + expect(subject[:summary]).to include(total: 1, resolved: 0, failed: 0, errored: 1) + subject[:new_errors].first.tap do |new_error| + expect(new_error[:status]).to eq(test_case_error.status) + expect(new_error[:name]).to eq(test_case_error.name) + expect(new_error[:execution_time]).to eq(test_case_error.execution_time) + expect(new_error[:system_output]).to eq(test_case_error.system_output) + end + expect(subject[:resolved_failures]).to be_empty + expect(subject[:existing_failures]).to be_empty + end + end + context 'when head suite still has a failed test case which failed in base' do before do base_suite.add_test_case(test_case_failed) @@ -46,7 +68,7 @@ describe TestSuiteComparerEntity do it 'contains correct compared test suite details' do expect(subject[:name]).to eq(name) expect(subject[:status]).to eq('failed') - expect(subject[:summary]).to include(total: 1, resolved: 0, failed: 1) + expect(subject[:summary]).to include(total: 1, resolved: 0, failed: 1, errored: 0) expect(subject[:new_failures]).to be_empty expect(subject[:resolved_failures]).to be_empty subject[:existing_failures].first.tap do |existing_failure| @@ -67,7 +89,7 @@ describe TestSuiteComparerEntity do it 'contains correct compared test suite details' do expect(subject[:name]).to eq(name) expect(subject[:status]).to eq('success') - expect(subject[:summary]).to include(total: 1, resolved: 1, failed: 0) + expect(subject[:summary]).to include(total: 1, resolved: 1, failed: 0, errored: 0) expect(subject[:new_failures]).to be_empty subject[:resolved_failures].first.tap do |resolved_failure| expect(resolved_failure[:status]).to eq(test_case_success.status) @@ -88,42 +110,57 @@ describe TestSuiteComparerEntity do context 'prefers new over existing and resolved' do before do 3.times { add_new_failure } + 3.times { add_new_error } 3.times { add_existing_failure } + 3.times { add_existing_error } 3.times { add_resolved_failure } + 3.times { add_resolved_error } end - it 'returns 2 new failures, and 1 of resolved and existing' do - expect(subject[:summary]).to include(total: 9, resolved: 3, failed: 6) + it 'returns 2 of each new category, and 1 of each resolved and existing' do + expect(subject[:summary]).to include(total: 18, resolved: 6, failed: 6, errored: 6) expect(subject[:new_failures].count).to eq(2) + expect(subject[:new_errors].count).to eq(2) expect(subject[:existing_failures].count).to eq(1) + expect(subject[:existing_errors].count).to eq(1) expect(subject[:resolved_failures].count).to eq(1) + expect(subject[:resolved_errors].count).to eq(1) end end context 'prefers existing over resolved' do before do 3.times { add_existing_failure } + 3.times { add_existing_error } 3.times { add_resolved_failure } + 3.times { add_resolved_error } end - it 'returns 2 existing failures, and 1 resolved' do - expect(subject[:summary]).to include(total: 6, resolved: 3, failed: 3) + it 'returns 2 of each existing category, and 1 of each resolved' do + expect(subject[:summary]).to include(total: 12, resolved: 6, failed: 3, errored: 3) expect(subject[:new_failures].count).to eq(0) + expect(subject[:new_errors].count).to eq(0) expect(subject[:existing_failures].count).to eq(2) + expect(subject[:existing_errors].count).to eq(2) expect(subject[:resolved_failures].count).to eq(1) + expect(subject[:resolved_errors].count).to eq(1) end end context 'limits amount of resolved' do before do 3.times { add_resolved_failure } + 3.times { add_resolved_error } end - it 'returns 2 resolved failures' do - expect(subject[:summary]).to include(total: 3, resolved: 3, failed: 0) + it 'returns 2 of each resolved category' do + expect(subject[:summary]).to include(total: 6, resolved: 6, failed: 0, errored: 0) expect(subject[:new_failures].count).to eq(0) + expect(subject[:new_errors].count).to eq(0) expect(subject[:existing_failures].count).to eq(0) + expect(subject[:existing_errors].count).to eq(0) expect(subject[:resolved_failures].count).to eq(2) + expect(subject[:resolved_errors].count).to eq(2) end end @@ -134,19 +171,38 @@ describe TestSuiteComparerEntity do head_suite.add_test_case(failed_case) end + def add_new_error + error_case = create_test_case_rspec_error(SecureRandom.hex) + head_suite.add_test_case(error_case) + end + def add_existing_failure failed_case = create_test_case_rspec_failed(SecureRandom.hex) base_suite.add_test_case(failed_case) head_suite.add_test_case(failed_case) end + def add_existing_error + error_case = create_test_case_rspec_error(SecureRandom.hex) + base_suite.add_test_case(error_case) + head_suite.add_test_case(error_case) + end + def add_resolved_failure case_name = SecureRandom.hex - failed_case = create_test_case_rspec_failed(case_name) - success_case = create_test_case_rspec_success(case_name) + failed_case = create_test_case_java_failed(case_name) + success_case = create_test_case_java_success(case_name) base_suite.add_test_case(failed_case) head_suite.add_test_case(success_case) end + + def add_resolved_error + case_name = SecureRandom.hex + error_case = create_test_case_java_error(case_name) + success_case = create_test_case_java_success(case_name) + base_suite.add_test_case(error_case) + head_suite.add_test_case(success_case) + end end end end diff --git a/spec/services/snippets/create_service_spec.rb b/spec/services/snippets/create_service_spec.rb index 7aa8f0ebf8f..1cf1cff51ed 100644 --- a/spec/services/snippets/create_service_spec.rb +++ b/spec/services/snippets/create_service_spec.rb @@ -143,6 +143,34 @@ describe Snippets::CreateService do end end + shared_examples 'creates repository' do + it do + subject + + expect(snippet.repository_exists?).to be_truthy + end + + context 'when snippet creation fails' do + let(:extra_opts) { { content: nil } } + + it 'does not create repository' do + subject + + expect(snippet.repository_exists?).to be_falsey + end + end + + context 'when feature flag :version_snippets is disabled' do + it 'does not create snippet repository' do + stub_feature_flags(version_snippets: false) + + subject + + expect(snippet.repository_exists?).to be_falsey + end + end + end + context 'when Project Snippet' do let_it_be(:project) { create(:project) } @@ -155,6 +183,7 @@ describe Snippets::CreateService do it_behaves_like 'spam check is performed' it_behaves_like 'snippet create data is tracked' it_behaves_like 'an error service response when save fails' + it_behaves_like 'creates repository' end context 'when PersonalSnippet' do @@ -165,6 +194,7 @@ describe Snippets::CreateService do it_behaves_like 'spam check is performed' it_behaves_like 'snippet create data is tracked' it_behaves_like 'an error service response when save fails' + it_behaves_like 'creates repository' end end end diff --git a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb index 2548afc1570..8092f87383d 100644 --- a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb +++ b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb @@ -123,7 +123,7 @@ RSpec.shared_examples 'cycle analytics label based stage' do }) expect(stage).to be_invalid - expect(stage.errors[:start_event_label]).to include("can't be blank") + expect(stage.errors[:start_event_label_id]).to include("can't be blank") end it 'returns validation error when `end_event_label_id` is missing' do @@ -135,7 +135,7 @@ RSpec.shared_examples 'cycle analytics label based stage' do }) expect(stage).to be_invalid - expect(stage.errors[:end_event_label]).to include("can't be blank") + expect(stage.errors[:end_event_label_id]).to include("can't be blank") end end @@ -145,7 +145,7 @@ RSpec.shared_examples 'cycle analytics label based stage' do name: 'My Stage', parent: parent, start_event_identifier: :issue_label_added, - start_event_label: group_label, + start_event_label_id: group_label.id, end_event_identifier: :issue_closed }) @@ -159,7 +159,7 @@ RSpec.shared_examples 'cycle analytics label based stage' do name: 'My Stage', parent: parent_in_subgroup, start_event_identifier: :issue_label_added, - start_event_label: group_label, + start_event_label_id: group_label.id, end_event_identifier: :issue_closed }) @@ -170,30 +170,30 @@ RSpec.shared_examples 'cycle analytics label based stage' do context 'when label is defined for a different group' do let(:error_message) { s_('CycleAnalyticsStage|is not available for the selected group') } - it 'returns validation for `start_event_label`' do + it 'returns validation for `start_event_label_id`' do stage = described_class.new({ name: 'My Stage', parent: parent_outside_of_group_label_scope, start_event_identifier: :issue_label_added, - start_event_label: group_label, + start_event_label_id: group_label.id, end_event_identifier: :issue_closed }) expect(stage).to be_invalid - expect(stage.errors[:start_event_label]).to include(error_message) + expect(stage.errors[:start_event_label_id]).to include(error_message) end - it 'returns validation for `end_event_label`' do + it 'returns validation for `end_event_label_id`' do stage = described_class.new({ name: 'My Stage', parent: parent_outside_of_group_label_scope, start_event_identifier: :issue_closed, end_event_identifier: :issue_label_added, - end_event_label: group_label + end_event_label_id: group_label.id }) expect(stage).to be_invalid - expect(stage.errors[:end_event_label]).to include(error_message) + expect(stage.errors[:end_event_label_id]).to include(error_message) end end |