Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-01 21:10:21 +00:00
parent 304e230182
commit 5b8f2c8a24
39 changed files with 462 additions and 153 deletions

View File

@ -1,12 +1,12 @@
<script>
/* eslint-disable vue/no-v-html */
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { GlModal } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import csrf from '~/lib/utils/csrf';
export default {
components: {
DeprecatedModal,
GlModal,
},
props: {
actionUrl: {
@ -55,21 +55,34 @@ You are about to permanently delete %{yourAccount}, and all of the issues, merge
Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
{
yourAccount: `<strong>${s__('Profiles|your account')}</strong>`,
deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`,
deleteAccount: `<strong>${s__('Profiles|Delete account')}</strong>`,
},
false,
);
},
},
methods: {
primaryProps() {
return {
text: s__('Delete account'),
attributes: [{ variant: 'danger' }, { category: 'primary' }, { disabled: !this.canSubmit }],
};
},
cancelProps() {
return {
text: s__('Cancel'),
};
},
canSubmit() {
if (this.confirmWithPassword) {
return this.enteredPassword !== '';
}
return this.enteredUsername === this.username;
},
},
methods: {
onSubmit() {
if (!this.canSubmit) {
return;
}
this.$refs.form.submit();
},
},
@ -77,42 +90,39 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
</script>
<template>
<deprecated-modal
id="delete-account-modal"
:title="s__('Profiles|Delete your account?')"
:text="text"
:primary-button-label="s__('Profiles|Delete account')"
:submit-disabled="!canSubmit()"
kind="danger"
@submit="onSubmit"
<gl-modal
modal-id="delete-account-modal"
title="Profiles"
:action-primary="primaryProps"
:action-cancel="cancelProps"
:ok-disabled="!canSubmit"
@primary="onSubmit"
>
<template #body="props">
<p v-html="props.text"></p>
<p v-html="text"></p>
<form ref="form" :action="actionUrl" method="post">
<input type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<form ref="form" :action="actionUrl" method="post">
<input type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<p id="input-label" v-html="inputLabel"></p>
<p id="input-label" v-html="inputLabel"></p>
<input
v-if="confirmWithPassword"
v-model="enteredPassword"
name="password"
class="form-control"
type="password"
data-qa-selector="password_confirmation_field"
aria-labelledby="input-label"
/>
<input
v-else
v-model="enteredUsername"
name="username"
class="form-control"
type="text"
aria-labelledby="input-label"
/>
</form>
</template>
</deprecated-modal>
<input
v-if="confirmWithPassword"
v-model="enteredPassword"
name="password"
class="form-control"
type="password"
data-qa-selector="password_confirmation_field"
aria-labelledby="input-label"
/>
<input
v-else
v-model="enteredUsername"
name="username"
class="form-control"
type="text"
aria-labelledby="input-label"
/>
</form>
</gl-modal>
</template>

View File

@ -30,6 +30,9 @@ export default () => {
},
mounted() {
deleteAccountButton.classList.remove('disabled');
deleteAccountButton.addEventListener('click', () => {
this.$root.$emit('bv::show::modal', 'delete-account-modal', '#delete-account-button');
});
},
render(createElement) {
return createElement('delete-account-modal', {

View File

@ -8,6 +8,7 @@ module Ci
include UpdateProjectStatistics
include Artifactable
include FileStoreMounter
include Presentable
FILE_STORE_SUPPORTED = [
ObjectStorage::Store::LOCAL,
@ -44,5 +45,9 @@ module Ci
def self.find_with_code_coverage
find_by(file_type: :code_coverage)
end
def present
super(presenter_class: "Ci::PipelineArtifacts::#{self.file_type.camelize}Presenter".constantize)
end
end
end

View File

@ -38,8 +38,12 @@ class Packages::Package < ApplicationRecord
validates :version, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? }
validates :version, format: { with: Gitlab::Regex.pypi_version_regex }, if: :pypi?
validates :version,
presence: true,
format: { with: Gitlab::Regex.generic_package_version_regex },
if: :generic?
enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6 }
enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6, generic: 7 }
scope :with_name, ->(name) { where(name: name) }
scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) }

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Ci
module PipelineArtifacts
class CodeCoveragePresenter < ProcessablePresenter
include Gitlab::Utils::StrongMemoize
def for_files(filenames)
coverage_files = raw_report["files"].select { |key| filenames.include?(key) }
{ files: coverage_files }
end
private
def raw_report
strong_memoize(:raw_report) do
self.each_blob do |blob|
Gitlab::Json.parse(blob)
end
end
end
end
end
end

View File

@ -13,7 +13,7 @@ class TestSuiteEntity < Grape::Entity
with_options if: -> (_, opts) { opts[:details] } do |test_suite|
expose :suite_error
expose :test_cases, using: TestCaseEntity do |test_suite|
test_suite.suite_error ? [] : test_suite.test_cases.values.flat_map(&:values)
test_suite.suite_error ? [] : test_suite.sorted.test_cases.values.flat_map(&:values)
end
end
end

View File

@ -12,7 +12,7 @@ module Ci
{
status: :parsed,
key: key(base_pipeline, head_pipeline),
data: Gitlab::Ci::Pipeline::Artifact::CodeCoverage.new(head_pipeline.pipeline_artifacts.find_with_code_coverage).for_files(merge_request.new_paths)
data: head_pipeline.pipeline_artifacts.find_with_code_coverage.present.for_files(merge_request.new_paths)
}
rescue => e
Gitlab::ErrorTracking.track_exception(e, project_id: project.id)

View File

@ -55,8 +55,8 @@
= s_('Profiles|Deleting an account has the following effects:')
= render 'users/deletion_guidance', user: current_user
%button#delete-account-button.btn.btn-danger.disabled{ data: { toggle: 'modal',
target: '#delete-account-modal', qa_selector: 'delete_account_button' } }
-# Delete button here
%button#delete-account-button.btn.btn-danger.disabled{ data: { qa_selector: 'delete_account_button' } }
= s_('Profiles|Delete account')
#delete-account-modal{ data: { action_url: user_registration_path,

View File

@ -0,0 +1,5 @@
---
title: Add new "generic" package type
merge_request: 40061
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Sort TestCase data by status and execution_time
merge_request: 40722
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add --if-exists to pg_dump command-line in backup creation
merge_request: 40792
author:
type: other

View File

@ -4,6 +4,7 @@ class CreateCiPlatformMetrics < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
CI_VARIABLES_KEY_INDEX_NAME = "index_ci_variables_on_key"
disable_ddl_transaction!
@ -17,11 +18,14 @@ class CreateCiPlatformMetrics < ActiveRecord::Migration[6.0]
end
add_text_limit :ci_platform_metrics, :platform_target, 255
add_concurrent_index :ci_variables, :key
add_concurrent_index :ci_variables, :key, name: CI_VARIABLES_KEY_INDEX_NAME
end
def down
drop_table :ci_platform_metrics
remove_concurrent_index :ci_variables, :key, name: 'index_ci_variables_on_key'
if table_exists?(:ci_platform_metrics)
drop_table :ci_platform_metrics
end
remove_concurrent_index :ci_variables, :key, name: CI_VARIABLES_KEY_INDEX_NAME
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class AddUniqueIndexForGenericPackages < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_packages_on_project_id_name_version_unique_when_generic'
PACKAGE_TYPE_GENERIC = 7
disable_ddl_transaction!
def up
add_concurrent_index :packages_packages, [:project_id, :name, :version], unique: true, where: "package_type = #{PACKAGE_TYPE_GENERIC}", name: INDEX_NAME
end
def down
remove_concurrent_index_by_name(:packages_packages, INDEX_NAME)
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddGenericPackageMaxFileSizeToPlanLimits < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column(:plan_limits, :generic_packages_max_file_size, :bigint, default: 5.gigabytes, null: false)
end
end

View File

@ -0,0 +1 @@
ddf3452bb44437324d20c9db03e998f8903f5ff9732d29cf85dd5d579507952d

View File

@ -0,0 +1 @@
4f3528d7df6e61c8b14911644f9223ac5f6e678184d1c8370d1e9a60389cd60c

View File

@ -14195,7 +14195,8 @@ CREATE TABLE public.plan_limits (
maven_max_file_size bigint DEFAULT 52428800 NOT NULL,
npm_max_file_size bigint DEFAULT 52428800 NOT NULL,
nuget_max_file_size bigint DEFAULT 52428800 NOT NULL,
pypi_max_file_size bigint DEFAULT 52428800 NOT NULL
pypi_max_file_size bigint DEFAULT 52428800 NOT NULL,
generic_packages_max_file_size bigint DEFAULT '5368709120'::bigint NOT NULL
);
CREATE SEQUENCE public.plan_limits_id_seq
@ -20414,6 +20415,8 @@ CREATE INDEX index_packages_maven_metadata_on_package_id_and_path ON public.pack
CREATE INDEX index_packages_nuget_dl_metadata_on_dependency_link_id ON public.packages_nuget_dependency_link_metadata USING btree (dependency_link_id);
CREATE UNIQUE INDEX index_packages_on_project_id_name_version_unique_when_generic ON public.packages_packages USING btree (project_id, name, version) WHERE (package_type = 7);
CREATE INDEX index_packages_package_files_file_store_is_null ON public.packages_package_files USING btree (id) WHERE (file_store IS NULL);
CREATE INDEX index_packages_package_files_on_file_store ON public.packages_package_files USING btree (file_store);

View File

@ -67,7 +67,6 @@ swap:
matt: matte
meagre: meager
metre: meter
mitre: miter
modelling: modeling
moustache: mustache
neighbour: neighbor

View File

@ -10565,6 +10565,11 @@ enum PackageTypeEnum {
"""
CONAN
"""
Packages from the generic package manager
"""
GENERIC
"""
Packages from the maven package manager
"""

View File

@ -31806,6 +31806,12 @@
"description": "Packages from the composer package manager",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "GENERIC",
"description": "Packages from the generic package manager",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null

View File

@ -27,6 +27,7 @@ module Backup
progress.print "Dumping PostgreSQL database #{config['database']} ... "
pg_env
pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump.
pgsql_args << '--if-exists'
if Gitlab.config.backup.pg_schema
pgsql_args << '-n'

View File

@ -1,33 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Ci
module Pipeline
module Artifact
class CodeCoverage
include Gitlab::Utils::StrongMemoize
def initialize(pipeline_artifact)
@pipeline_artifact = pipeline_artifact
end
def for_files(filenames)
coverage_files = raw_report["files"].select { |key| filenames.include?(key) }
{ files: coverage_files }
end
private
def raw_report
strong_memoize(:raw_report) do
@pipeline_artifact.each_blob do |blob|
Gitlab::Json.parse(blob)
end
end
end
end
end
end
end
end

View File

@ -8,7 +8,7 @@ module Gitlab
STATUS_FAILED = 'failed'
STATUS_SKIPPED = 'skipped'
STATUS_ERROR = 'error'
STATUS_TYPES = [STATUS_SUCCESS, STATUS_FAILED, STATUS_SKIPPED, STATUS_ERROR].freeze
STATUS_TYPES = [STATUS_ERROR, STATUS_FAILED, STATUS_SUCCESS, STATUS_SKIPPED].freeze
attr_reader :name, :classname, :execution_time, :status, :file, :system_output, :stack_trace, :key, :attachment, :job

View File

@ -78,11 +78,27 @@ module Gitlab
end
end
def sorted
sort_by_status
sort_by_execution_time_desc
self
end
private
def existing_key?(test_case)
@test_cases[test_case.status]&.key?(test_case.key)
end
def sort_by_status
@test_cases = @test_cases.sort_by { |status, _| Gitlab::Ci::Reports::TestCase::STATUS_TYPES.index(status) }.to_h
end
def sort_by_execution_time_desc
@test_cases = @test_cases.keys.each_with_object({}) do |key, hash|
hash[key] = @test_cases[key].sort_by { |_key, test_case| -test_case.execution_time }.to_h
end
end
end
end
end

View File

@ -104,6 +104,10 @@ module Gitlab
\b (?# word boundary)
/ix.freeze
end
def generic_package_version_regex
/\A\d+\.\d+\.\d+\z/
end
end
extend self

View File

@ -7947,6 +7947,9 @@ msgstr ""
msgid "Delete Snippet"
msgstr ""
msgid "Delete account"
msgstr ""
msgid "Delete artifacts"
msgstr ""
@ -18724,15 +18727,9 @@ msgstr ""
msgid "Profiles|Default notification email"
msgstr ""
msgid "Profiles|Delete Account"
msgstr ""
msgid "Profiles|Delete account"
msgstr ""
msgid "Profiles|Delete your account?"
msgstr ""
msgid "Profiles|Deleting an account has the following effects:"
msgstr ""

View File

@ -10,8 +10,12 @@ module RuboCop
MSG = 'indexes added with custom options must be explicitly named'
def_node_matcher :match_create_table_index_with_options, <<~PATTERN
(send _ {:index } _ (hash $...))
PATTERN
def_node_matcher :match_add_index_with_options, <<~PATTERN
(send _ {:add_concurrent_index} _ _ (hash $...))
(send _ {:add_index :add_concurrent_index} _ _ (hash $...))
PATTERN
def_node_matcher :name_option?, <<~PATTERN
@ -26,7 +30,7 @@ module RuboCop
return unless in_migration?(node)
node.each_descendant(:send) do |send_node|
next unless add_index_offense?(send_node)
next unless create_table_with_index_offense?(send_node) || add_index_offense?(send_node)
add_offense(send_node, location: :selector)
end
@ -34,6 +38,10 @@ module RuboCop
private
def create_table_with_index_offense?(send_node)
match_create_table_index_with_options(send_node) { |option_nodes| needs_name_option?(option_nodes) }
end
def add_index_offense?(send_node)
match_add_index_with_options(send_node) { |option_nodes| needs_name_option?(option_nodes) }
end

View File

@ -121,6 +121,12 @@ FactoryBot.define do
conan_metadatum { build(:conan_metadatum, package: nil) }
end
end
factory :generic_package do
sequence(:name) { |n| "generic-package-#{n}" }
version { '1.0.0' }
package_type { :generic }
end
end
factory :composer_metadatum, class: 'Packages::Composer::Metadatum' do

View File

@ -1,21 +1,49 @@
import Vue from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import mountComponent from 'helpers/vue_mount_component_helper';
import { merge } from 'lodash';
import { mount } from '@vue/test-utils';
import deleteAccountModal from '~/profile/account/components/delete_account_modal.vue';
const GlModalStub = {
name: 'gl-modal-stub',
template: `
<div>
<slot></slot>
</div>
`,
};
describe('DeleteAccountModal component', () => {
const actionUrl = `${TEST_HOST}/delete/user`;
const username = 'hasnoname';
let Component;
let wrapper;
let vm;
beforeEach(() => {
Component = Vue.extend(deleteAccountModal);
});
const createWrapper = (options = {}) => {
wrapper = mount(
deleteAccountModal,
merge(
{},
{
propsData: {
actionUrl,
username,
},
stubs: {
GlModal: GlModalStub,
},
},
options,
),
);
vm = wrapper.vm;
};
afterEach(() => {
vm.$destroy();
wrapper.destroy();
wrapper = null;
vm = null;
});
const findElements = () => {
@ -23,16 +51,16 @@ describe('DeleteAccountModal component', () => {
return {
form: vm.$refs.form,
input: vm.$el.querySelector(`[name="${confirmation}"]`),
submitButton: vm.$el.querySelector('.btn-danger'),
};
};
const findModal = () => wrapper.find(GlModalStub);
describe('with password confirmation', () => {
beforeEach(done => {
vm = mountComponent(Component, {
actionUrl,
confirmWithPassword: true,
username,
createWrapper({
propsData: {
confirmWithPassword: true,
},
});
vm.isOpen = true;
@ -43,7 +71,7 @@ describe('DeleteAccountModal component', () => {
});
it('does not accept empty password', done => {
const { form, input, submitButton } = findElements();
const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = '';
input.dispatchEvent(new Event('input'));
@ -51,8 +79,8 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredPassword).toBe(input.value);
expect(submitButton).toHaveAttr('disabled', 'disabled');
submitButton.click();
expect(findModal().attributes('ok-disabled')).toBe('true');
findModal().vm.$emit('primary');
expect(form.submit).not.toHaveBeenCalled();
})
@ -61,7 +89,7 @@ describe('DeleteAccountModal component', () => {
});
it('submits form with password', done => {
const { form, input, submitButton } = findElements();
const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = 'anything';
input.dispatchEvent(new Event('input'));
@ -69,8 +97,8 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredPassword).toBe(input.value);
expect(submitButton).not.toHaveAttr('disabled', 'disabled');
submitButton.click();
expect(findModal().attributes('ok-disabled')).toBeUndefined();
findModal().vm.$emit('primary');
expect(form.submit).toHaveBeenCalled();
})
@ -81,10 +109,10 @@ describe('DeleteAccountModal component', () => {
describe('with username confirmation', () => {
beforeEach(done => {
vm = mountComponent(Component, {
actionUrl,
confirmWithPassword: false,
username,
createWrapper({
propsData: {
confirmWithPassword: false,
},
});
vm.isOpen = true;
@ -95,7 +123,7 @@ describe('DeleteAccountModal component', () => {
});
it('does not accept wrong username', done => {
const { form, input, submitButton } = findElements();
const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = 'this is wrong';
input.dispatchEvent(new Event('input'));
@ -103,8 +131,8 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredUsername).toBe(input.value);
expect(submitButton).toHaveAttr('disabled', 'disabled');
submitButton.click();
expect(findModal().attributes('ok-disabled')).toBe('true');
findModal().vm.$emit('primary');
expect(form.submit).not.toHaveBeenCalled();
})
@ -113,7 +141,7 @@ describe('DeleteAccountModal component', () => {
});
it('submits form with correct username', done => {
const { form, input, submitButton } = findElements();
const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = username;
input.dispatchEvent(new Event('input'));
@ -121,8 +149,8 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredUsername).toBe(input.value);
expect(submitButton).not.toHaveAttr('disabled', 'disabled');
submitButton.click();
expect(findModal().attributes('ok-disabled')).toBeUndefined();
findModal().vm.$emit('primary');
expect(form.submit).toHaveBeenCalled();
})

View File

@ -4,6 +4,6 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['PackageTypeEnum'] do
it 'exposes all package types' do
expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER])
expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC])
end
end

View File

@ -176,6 +176,37 @@ RSpec.describe Gitlab::Ci::Reports::TestSuite do
end
end
describe '#sorted' do
subject { test_suite.sorted }
context 'when there are multiple failed test cases' do
before do
test_suite.add_test_case(create_test_case_rspec_failed('test_spec_1', 1.11))
test_suite.add_test_case(create_test_case_rspec_failed('test_spec_2', 4.44))
end
it 'returns test cases sorted by execution time desc' do
expect(subject.test_cases['failed'].each_value.first.execution_time).to eq(4.44)
expect(subject.test_cases['failed'].values.second.execution_time).to eq(1.11)
end
end
context 'when there are multiple test cases' do
let(:status_ordered) { %w(error failed success skipped) }
before do
test_suite.add_test_case(test_case_success)
test_suite.add_test_case(test_case_failed)
test_suite.add_test_case(test_case_error)
test_suite.add_test_case(test_case_skipped)
end
it 'returns test cases sorted by status' do
expect(subject.test_cases.keys).to eq(status_ordered)
end
end
end
Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type|
describe "##{status_type}" do
subject { test_suite.public_send("#{status_type}") }

View File

@ -426,4 +426,21 @@ RSpec.describe Gitlab::Regex do
it { is_expected.not_to match('1.2') }
it { is_expected.not_to match('1./2.3') }
end
describe '.generic_package_version_regex' do
subject { described_class.generic_package_version_regex }
it { is_expected.to match('1.2.3') }
it { is_expected.to match('1.3.350') }
it { is_expected.not_to match('1.3.350-20201230123456') }
it { is_expected.not_to match('..1.2.3') }
it { is_expected.not_to match(' 1.2.3') }
it { is_expected.not_to match("1.2.3 \r\t") }
it { is_expected.not_to match("\r\t 1.2.3") }
it { is_expected.not_to match('1.2.3-4/../../') }
it { is_expected.not_to match('1.2.3-4%2e%2e%') }
it { is_expected.not_to match('../../../../../1.2.3') }
it { is_expected.not_to match('%2e%2e%2f1.2.3') }
it { is_expected.not_to match('') }
end
end

View File

@ -109,4 +109,14 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
end
end
end
describe '#present' do
subject { coverage_report.present }
context 'when file_type is code_coverage' do
it 'uses code coverage presenter' do
expect(subject.present).to be_kind_of(Ci::PipelineArtifacts::CodeCoveragePresenter)
end
end
end
end

View File

@ -236,6 +236,25 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) }
end
context 'generic package' do
subject { build_stubbed(:generic_package) }
it { is_expected.to validate_presence_of(:version) }
it { is_expected.to allow_value('1.2.3').for(:version) }
it { is_expected.to allow_value('1.3.350').for(:version) }
it { is_expected.not_to allow_value('1.3.350-20201230123456').for(:version) }
it { is_expected.not_to allow_value('..1.2.3').for(:version) }
it { is_expected.not_to allow_value(' 1.2.3').for(:version) }
it { is_expected.not_to allow_value("1.2.3 \r\t").for(:version) }
it { is_expected.not_to allow_value("\r\t 1.2.3").for(:version) }
it { is_expected.not_to allow_value('1.2.3-4/../../').for(:version) }
it { is_expected.not_to allow_value('1.2.3-4%2e%2e%').for(:version) }
it { is_expected.not_to allow_value('../../../../../1.2.3').for(:version) }
it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) }
it { is_expected.not_to allow_value('').for(:version) }
it { is_expected.not_to allow_value(nil).for(:version) }
end
it_behaves_like 'validating version to be SemVer compliant for', :npm_package
it_behaves_like 'validating version to be SemVer compliant for', :nuget_package
end
@ -552,11 +571,17 @@ RSpec.describe Packages::Package, type: :model do
describe 'plan_limits' do
Packages::Package.package_types.keys.without('composer').each do |pt|
plan_limit_name = if pt == 'generic'
"#{pt}_packages_max_file_size"
else
"#{pt}_max_file_size"
end
context "File size limits for #{pt}" do
let(:package) { create("#{pt}_package") }
it "plan_limits includes column #{pt}_max_file_size" do
expect { package.project.actual_limits.send("#{pt}_max_file_size") }
it "plan_limits includes column #{plan_limit_name}" do
expect { package.project.actual_limits.send(plan_limit_name) }
.not_to raise_error(NoMethodError)
end
end

View File

@ -2,12 +2,13 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Artifact::CodeCoverage do
RSpec.describe Ci::PipelineArtifacts::CodeCoveragePresenter do
let(:pipeline_artifact) { create(:ci_pipeline_artifact, :with_code_coverage_with_multiple_files) }
let(:code_coverage) { described_class.new(pipeline_artifact) }
subject(:presenter) { described_class.new(pipeline_artifact) }
describe '#for_files' do
subject { code_coverage.for_files(filenames) }
subject { presenter.for_files(filenames) }
context 'when code coverage has data' do
context 'when filenames is empty' do

View File

@ -14,40 +14,120 @@ RSpec.describe RuboCop::Cop::Migration::ComplexIndexesRequireName, type: :ruboco
allow(cop).to receive(:in_migration?).and_return(true)
end
context 'when indexes are configured with an options hash, but no name' do
it 'registers an offense' do
expect_offense(<<~RUBY)
class TestComplexIndexes < ActiveRecord::Migration[6.0]
DOWNTIME = false
context 'when creating complex indexes as part of create_table' do
context 'when indexes are configured with an options hash, but no name' do
it 'registers an offense' do
expect_offense(<<~RUBY)
class TestComplexIndexes < ActiveRecord::Migration[6.0]
DOWNTIME = false
INDEX_NAME = 'my_test_name'
def up
create_table :test_table do |t|
t.integer :column1, null: false
t.integer :column2, null: false
t.jsonb :column3
disable_ddl_transaction!
t.index :column1, unique: true
t.index :column2, where: 'column1 = 0'
^^^^^ #{described_class::MSG}
t.index :column3, using: :gin
^^^^^ #{described_class::MSG}
end
end
def up
add_concurrent_index :test_indexes, :column1
add_concurrent_index :test_indexes, :column2, where: "column2 = 'value'", order: { column4: :desc }
^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
add_concurrent_index :test_indexes, :column3, where: 'column3 = 10', name: 'idx_equal_to_10'
def down
drop_table :test_table
end
end
RUBY
def down
add_concurrent_index :test_indexes, :column4, 'unique' => true
expect(cop.offenses.map(&:cop_name)).to all(eq("Migration/#{described_class.name.demodulize}"))
end
end
add_concurrent_index :test_indexes, :column4, 'unique' => true, where: 'column4 IS NOT NULL'
^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
context 'when indexes are configured with an options hash and name' do
it 'registers no offense' do
expect_no_offenses(<<~RUBY)
class TestComplexIndexes < ActiveRecord::Migration[6.0]
DOWNTIME = false
add_concurrent_index :test_indexes, :column5, using: :gin, name: INDEX_NAME
def up
create_table :test_table do |t|
t.integer :column1, null: false
t.integer :column2, null: false
t.jsonb :column3
add_concurrent_index :test_indexes, :column6, using: :gin, opclass: :gin_trgm_ops
^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
t.index :column1, unique: true
t.index :column2, where: 'column1 = 0', name: 'my_index_1'
t.index :column3, using: :gin, name: 'my_gin_index'
end
end
def down
drop_table :test_table
end
end
end
RUBY
RUBY
end
end
end
expect(cop.offenses.map(&:cop_name)).to all(eq("Migration/#{described_class.name.demodulize}"))
context 'when indexes are added to an existing table' do
context 'when indexes are configured with an options hash, but no name' do
it 'registers an offense' do
expect_offense(<<~RUBY)
class TestComplexIndexes < ActiveRecord::Migration[6.0]
DOWNTIME = false
disable_ddl_transaction!
def up
add_index :test_indexes, :column1
add_index :test_indexes, :column2, where: "column2 = 'value'", order: { column4: :desc }
^^^^^^^^^ #{described_class::MSG}
end
def down
add_index :test_indexes, :column4, 'unique' => true, where: 'column4 IS NOT NULL'
^^^^^^^^^ #{described_class::MSG}
add_concurrent_index :test_indexes, :column6, using: :gin, opclass: :gin_trgm_ops
^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
end
end
RUBY
expect(cop.offenses.map(&:cop_name)).to all(eq("Migration/#{described_class.name.demodulize}"))
end
end
context 'when indexes are configured with an options hash and a name' do
it 'registers no offenses' do
expect_no_offenses(<<~RUBY)
class TestComplexIndexes < ActiveRecord::Migration[6.0]
DOWNTIME = false
INDEX_NAME = 'my_test_name'
disable_ddl_transaction!
def up
add_index :test_indexes, :column1
add_index :test_indexes, :column2, where: "column2 = 'value'", order: { column4: :desc }, name: 'my_index_1'
add_concurrent_index :test_indexes, :column3, where: 'column3 = 10', name: 'idx_equal_to_10'
end
def down
add_index :test_indexes, :column4, 'unique' => true, where: 'column4 IS NOT NULL', name: 'my_index_2'
add_concurrent_index :test_indexes, :column6, using: :gin, opclass: :gin_trgm_ops, name: INDEX_NAME
end
end
RUBY
end
end
end
end
@ -65,7 +145,13 @@ RSpec.describe RuboCop::Cop::Migration::ComplexIndexesRequireName, type: :ruboco
disable_ddl_transaction!
def up
add_concurrent_index :test_indexes, :column1, where: "some_column = 'value'"
create_table :test_table do |t|
t.integer :column1
t.index :column1, where: 'column2 IS NOT NULL'
end
add_index :test_indexes, :column1, where: "some_column = 'value'"
end
def down

View File

@ -16,7 +16,8 @@ RSpec.describe Ci::GenerateCoverageReportsService do
let!(:base_pipeline) { nil }
it 'returns status and data', :aggregate_failures do
expect_next_instance_of(Gitlab::Ci::Pipeline::Artifact::CodeCoverage) do |instance|
expect_any_instance_of(Ci::PipelineArtifact) do |instance|
expect(instance).to receive(:present)
expect(instance).to receive(:for_files).with(merge_request.new_paths).and_call_original
end

View File

@ -161,6 +161,7 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false|
let_it_be(:package4) { create(:nuget_package, project: project) }
let_it_be(:package5) { create(:pypi_package, project: project) }
let_it_be(:package6) { create(:composer_package, project: project) }
let_it_be(:package7) { create(:generic_package, project: project) }
Packages::Package.package_types.keys.each do |package_type|
context "for package type #{package_type}" do

View File

@ -10,12 +10,12 @@ module TestReportsHelper
status: Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
def create_test_case_rspec_failed(name = 'test_spec')
def create_test_case_rspec_failed(name = 'test_spec', execution_time = 2.22)
Gitlab::Ci::Reports::TestCase.new(
name: 'Test#sum when a is 1 and b is 3 returns summary',
classname: "spec.#{name}",
file: './spec/test_spec.rb',
execution_time: 2.22,
execution_time: execution_time,
system_output: sample_rspec_failed_message,
status: Gitlab::Ci::Reports::TestCase::STATUS_FAILED)
end