Commit 319756b5 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce_upstream' into 'master'

CE upstream

Closes omnibus-gitlab#2988 et gitlab-com/infrastructure#3260

See merge request gitlab-org/gitlab-ee!3570
parents 833954a7 f308127d
...@@ -604,7 +604,7 @@ codequality: ...@@ -604,7 +604,7 @@ codequality:
script: script:
- cp .rubocop.yml .rubocop.yml.bak - cp .rubocop.yml .rubocop.yml.bak
- grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml - grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > raw_codeclimate.json - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
- mv .rubocop.yml.bak .rubocop.yml - mv .rubocop.yml.bak .rubocop.yml
artifacts: artifacts:
......
...@@ -114,7 +114,7 @@ gem 'google-api-client', '~> 0.13.6' ...@@ -114,7 +114,7 @@ gem 'google-api-client', '~> 0.13.6'
gem 'unf', '~> 0.1.4' gem 'unf', '~> 0.1.4'
# Seed data # Seed data
gem 'seed-fu', '~> 2.3.5' gem 'seed-fu', '~> 2.3.7'
# Search # Search
gem 'elasticsearch-model', '~> 0.1.9' gem 'elasticsearch-model', '~> 0.1.9'
...@@ -295,7 +295,7 @@ group :metrics do ...@@ -295,7 +295,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false gem 'influxdb', '~> 0.2', require: false
# Prometheus # Prometheus
gem 'prometheus-client-mmap', '~> 0.7.0.beta37' gem 'prometheus-client-mmap', '~> 0.7.0.beta39'
gem 'raindrops', '~> 0.18' gem 'raindrops', '~> 0.18'
end end
......
...@@ -654,7 +654,7 @@ GEM ...@@ -654,7 +654,7 @@ GEM
parser parser
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.7.0.beta37) prometheus-client-mmap (0.7.0.beta39)
mmap2 (~> 2.2, >= 2.2.9) mmap2 (~> 2.2, >= 2.2.9)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
...@@ -844,7 +844,7 @@ GEM ...@@ -844,7 +844,7 @@ GEM
rake (>= 0.9, < 13) rake (>= 0.9, < 13)
sass (~> 3.4.20) sass (~> 3.4.20)
securecompare (1.0.0) securecompare (1.0.0)
seed-fu (2.3.6) seed-fu (2.3.7)
activerecord (>= 3.1) activerecord (>= 3.1)
activesupport (>= 3.1) activesupport (>= 3.1)
select2-rails (3.5.9.3) select2-rails (3.5.9.3)
...@@ -1149,7 +1149,7 @@ DEPENDENCIES ...@@ -1149,7 +1149,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3) peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2) pg (~> 0.18.2)
premailer-rails (~> 1.9.7) premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta37) prometheus-client-mmap (~> 0.7.0.beta39)
pry-byebug (~> 3.4.1) pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1) rack-attack (~> 4.4.1)
......
...@@ -56,9 +56,11 @@ export const slugify = str => str.trim().toLowerCase(); ...@@ -56,9 +56,11 @@ export const slugify = str => str.trim().toLowerCase();
export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`; export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`;
/** /**
* Capitalizes first character. * Capitalizes first character
* *
* @param {String} text * @param {String} text
* @returns {String} * @return {String}
*/ */
export const capitalizeFirstCharacter = text => `${text[0].toUpperCase()}${text.slice(1)}`; export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`;
}
...@@ -12,6 +12,9 @@ ...@@ -12,6 +12,9 @@
/> />
*/ */
// only allow classes in images.scss e.g. s12
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
export default { export default {
props: { props: {
name: { name: {
...@@ -22,7 +25,10 @@ ...@@ -22,7 +25,10 @@
size: { size: {
type: Number, type: Number,
required: false, required: false,
default: 0, default: 16,
validator(value) {
return validSizes.includes(value);
},
}, },
cssClasses: { cssClasses: {
...@@ -42,10 +48,11 @@ ...@@ -42,10 +48,11 @@
}, },
}; };
</script> </script>
<template> <template>
<svg <svg
:class="[iconSizeClass, cssClasses]"> :class="[iconSizeClass, cssClasses]">
<use <use
v-bind="{'xlink:href':spriteHref}"/> v-bind="{'xlink:href':spriteHref}"/>
</svg> </svg>
</template> </template>
...@@ -292,6 +292,8 @@ ...@@ -292,6 +292,8 @@
.gutter-toggle { .gutter-toggle {
margin-top: 7px; margin-top: 7px;
border-left: 1px solid $border-gray-normal; border-left: 1px solid $border-gray-normal;
padding-left: 0;
text-align: center;
} }
.title .gutter-toggle { .title .gutter-toggle {
......
...@@ -54,7 +54,7 @@ module IssuableActions ...@@ -54,7 +54,7 @@ module IssuableActions
end end
def destroy def destroy
issuable.destroy Issuable::DestroyService.new(issuable.project, current_user).execute(issuable)
TodoService.new.destroy_issuable(issuable, current_user) TodoService.new.destroy_issuable(issuable, current_user)
name = issuable.human_class_name name = issuable.human_class_name
......
...@@ -45,8 +45,7 @@ class Projects::CommitsController < Projects::ApplicationController ...@@ -45,8 +45,7 @@ class Projects::CommitsController < Projects::ApplicationController
private private
def set_commits def set_commits
render_404 unless request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present? render_404 unless @path.empty? || request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present?
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
search = params[:search] search = params[:search]
......
class RunnerJobsFinder
attr_reader :runner, :params
def initialize(runner, params = {})
@runner = runner
@params = params
end
def execute
items = @runner.builds
items = by_status(items)
items
end
private
def by_status(items)
return items unless HasStatus::AVAILABLE_STATUSES.include?(params[:status])
items.where(status: params[:status])
end
end
...@@ -108,6 +108,7 @@ module Ci ...@@ -108,6 +108,7 @@ module Ci
end end
before_transition any => [:failed] do |build| before_transition any => [:failed] do |build|
next unless build.project
next if build.retries_max.zero? next if build.retries_max.zero?
if build.retries_count < build.retries_max if build.retries_count < build.retries_max
......
...@@ -17,6 +17,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -17,6 +17,7 @@ class CommitStatus < ActiveRecord::Base
validates :name, presence: true, unless: :importing? validates :name, presence: true, unless: :importing?
alias_attribute :author, :user alias_attribute :author, :user
alias_attribute :pipeline_id, :commit_id
scope :failed_but_allowed, -> do scope :failed_but_allowed, -> do
where(allow_failure: true, status: [:failed, :canceled]) where(allow_failure: true, status: [:failed, :canceled])
...@@ -103,26 +104,29 @@ class CommitStatus < ActiveRecord::Base ...@@ -103,26 +104,29 @@ class CommitStatus < ActiveRecord::Base
end end
after_transition do |commit_status, transition| after_transition do |commit_status, transition|
next unless commit_status.project
next if transition.loopback? next if transition.loopback?
commit_status.run_after_commit do commit_status.run_after_commit do
if pipeline if pipeline_id
if complete? || manual? if complete? || manual?
PipelineProcessWorker.perform_async(pipeline.id) PipelineProcessWorker.perform_async(pipeline_id)
else else
PipelineUpdateWorker.perform_async(pipeline.id) PipelineUpdateWorker.perform_async(pipeline_id)
end end
end end
StageUpdateWorker.perform_async(commit_status.stage_id) StageUpdateWorker.perform_async(stage_id)
ExpireJobCacheWorker.perform_async(commit_status.id) ExpireJobCacheWorker.perform_async(id)
end end
end end
after_transition any => :failed do |commit_status| after_transition any => :failed do |commit_status|
next unless commit_status.project
commit_status.run_after_commit do commit_status.run_after_commit do
MergeRequests::AddTodoWhenBuildFailsService MergeRequests::AddTodoWhenBuildFailsService
.new(pipeline.project, nil).execute(self) .new(project, nil).execute(self)
end end
end end
end end
......
...@@ -64,7 +64,6 @@ class Issue < ActiveRecord::Base ...@@ -64,7 +64,6 @@ class Issue < ActiveRecord::Base
scope :public_only, -> { where(confidential: false) } scope :public_only, -> { where(confidential: false) }
after_save :expire_etag_cache after_save :expire_etag_cache
after_commit :update_project_counter_caches, on: :destroy
attr_spammable :title, spam_title: true attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true attr_spammable :description, spam_description: true
......
...@@ -56,7 +56,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -56,7 +56,6 @@ class MergeRequest < ActiveRecord::Base
after_create :ensure_merge_request_diff, unless: :importing? after_create :ensure_merge_request_diff, unless: :importing?
after_update :reload_diff_if_branch_changed after_update :reload_diff_if_branch_changed
after_commit :update_project_counter_caches, on: :destroy
# When this attribute is true some MR validation is ignored # When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests # It allows us to close or modify broken merge requests
......
module Issuable
class DestroyService < IssuableBaseService
def execute(issuable)
if issuable.destroy
issuable.update_project_counter_caches
end
end
end
end
...@@ -45,9 +45,17 @@ class StuckCiJobsWorker ...@@ -45,9 +45,17 @@ class StuckCiJobsWorker
end end
def search(status, timeout) def search(status, timeout)
builds = Ci::Build.where(status: status).where('ci_builds.updated_at < ?', timeout.ago) loop do
builds.joins(:project).merge(Project.without_deleted).includes(:tags, :runner, project: :namespace).find_each(batch_size: 50).each do |build| jobs = Ci::Build.where(status: status)
yield(build) .where('ci_builds.updated_at < ?', timeout.ago)
.includes(:tags, :runner, project: :namespace)
.limit(100)
.to_a
break if jobs.empty?
jobs.each do |job|
yield(job)
end
end end
end end
......
---
title: Create issuable destroy service
merge_request: 15604
author: George Andrinopoulos
type: other
---
title: Upgrade seed-fu to 2.3.7
merge_request: 15607
author: Takuya Noguchi
type: other
---
title: Optimise StuckCiJobsWorker using cheap SQL query outside, and expensive inside
merge_request:
author:
type: performance
---
title: New API endpoint - list jobs for a specified runner
merge_request: 15432
author:
type: added
...@@ -215,6 +215,91 @@ DELETE /runners/:id ...@@ -215,6 +215,91 @@ DELETE /runners/:id
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6" curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6"
``` ```
## List runner's jobs
List jobs that are being processed or were processed by specified Runner.
```
GET /runners/:id/jobs
```
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a runner |
| `status` | string | no | Status of the job; one of: `running`, `success`, `failed`, `canceled` |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/1/jobs?status=running"
```
Example response:
```json
[
{
"id": 2,
"status": "running",
"stage": "test",
"name": "test",
"ref": "master",
"tag": false,
"coverage": null,
"created_at": "2017-11-16T08:50:29.000Z",
"started_at": "2017-11-16T08:51:29.000Z",
"finished_at": "2017-11-16T08:53:29.000Z",
"duration": 120,
"user": {
"id": 1,
"name": "John Doe2",
"username": "user2",
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
"web_url": "http://localhost/user2",
"created_at": "2017-11-16T18:38:46.000Z",
"bio": null,
"location": null,
"skype": "",
"linkedin": "",
"twitter": "",
"website_url": "",
"organization": null
},
"commit": {
"id": "97de212e80737a608d939f648d959671fb0a0142",
"short_id": "97de212e",
"title": "Update configuration\r",
"created_at": "2017-11-16T08:50:28.000Z",
"parent_ids": [
"1b12f15a11fc6e62177bef08f47bc7b5ce50b141",
"498214de67004b1da3d820901307bed2a68a8ef6"
],
"message": "See merge request !123",
"author_name": "John Doe2",
"author_email": "user2@example.org",
"authored_date": "2017-11-16T08:50:27.000Z",
"committer_name": "John Doe2",
"committer_email": "user2@example.org",
"committed_date": "2017-11-16T08:50:27.000Z"
},
"pipeline": {
"id": 2,
"sha": "97de212e80737a608d939f648d959671fb0a0142",
"ref": "master",
"status": "running"
},
"project": {
"id": 1,
"description": null,
"name": "project1",
"name_with_namespace": "John Doe2 / project1",
"path": "project1",
"path_with_namespace": "namespace1/project1",
"created_at": "2017-11-16T18:38:46.620Z"
}
}
]
```
## List project's runners ## List project's runners
List all runners (specific and shared) available in the project. Shared runners List all runners (specific and shared) available in the project. Shared runners
......
...@@ -82,7 +82,7 @@ added directly to your configured cluster. Those applications are needed for ...@@ -82,7 +82,7 @@ added directly to your configured cluster. Those applications are needed for
| Application | GitLab version | Description | | Application | GitLab version | Description |
| ----------- | :------------: | ----------- | | ----------- | :------------: | ----------- |
| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | | [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. |
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. | | [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. |
## Enabling or disabling the Cluster integration ## Enabling or disabling the Cluster integration
......
...@@ -90,16 +90,21 @@ module API ...@@ -90,16 +90,21 @@ module API
expose :group_access, as: :group_access_level expose :group_access, as: :group_access_level
end end
class BasicProjectDetails < Grape::Entity class ProjectIdentity < Grape::Entity
expose :id, :description, :default_branch, :tag_list expose :id, :description
expose :ssh_url_to_repo, :http_url_to_repo, :web_url
expose :name, :name_with_namespace expose :name, :name_with_namespace
expose :path, :path_with_namespace expose :path, :path_with_namespace
expose :created_at
end
class BasicProjectDetails < ProjectIdentity
expose :default_branch, :tag_list
expose :ssh_url_to_repo, :http_url_to_repo, :web_url
expose :avatar_url do |project, options| expose :avatar_url do |project, options|
project.avatar_url(only_path: false) project.avatar_url(only_path: false)
end end
expose :star_count, :forks_count expose :star_count, :forks_count
expose :created_at, :last_activity_at expose :last_activity_at
end end
class Project < BasicProjectDetails class Project < BasicProjectDetails
...@@ -938,17 +943,24 @@ module API ...@@ -938,17 +943,24 @@ module API
expose :id, :sha, :ref, :status expose :id, :sha, :ref, :status
end end
class Job < Grape::Entity class JobBasic < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at expose :created_at, :started_at, :finished_at
expose :duration expose :duration
expose :user, with: User expose :user, with: User
expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :commit, with: Commit expose :commit, with: Commit
expose :runner, with: Runner
expose :pipeline, with: PipelineBasic expose :pipeline, with: PipelineBasic
end end
class Job < JobBasic
expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :runner, with: Runner
end
class JobBasicWithProject < JobBasic
expose :project, with: ProjectIdentity
end
class Trigger < Grape::Entity class Trigger < Grape::Entity
expose :id expose :id
expose :token, :description expose :token, :description
......
...@@ -261,7 +261,9 @@ module API ...@@ -261,7 +261,9 @@ module API
authorize!(:destroy_issue, issue) authorize!(:destroy_issue, issue)
destroy_conditionally!(issue) destroy_conditionally!(issue) do |issue|
Issuable::DestroyService.new(user_project, current_user).execute(issue)
end
end end
desc 'List merge requests closing issue' do desc 'List merge requests closing issue' do
......
...@@ -179,7 +179,9 @@ module API ...@@ -179,7 +179,9 @@ module API
authorize!(:destroy_merge_request, merge_request) authorize!(:destroy_merge_request, merge_request)
destroy_conditionally!(merge_request) destroy_conditionally!(merge_request) do |merge_request|
Issuable::DestroyService.new(user_project, current_user).execute(merge_request)
end
end end
params do params do
......
...@@ -84,6 +84,23 @@ module API ...@@ -84,6 +84,23 @@ module API
destroy_conditionally!(runner) destroy_conditionally!(runner)
end end
desc 'List jobs running on a runner' do
success Entities::JobBasicWithProject
end
params do
requires :id, type: Integer, desc: 'The ID of the runner'
optional :status, type: String, desc: 'Status of the job', values: Ci::Build::AVAILABLE_STATUSES
use :pagination
end
get ':id/jobs' do
runner = get_runner(params[:id])
authenticate_list_runners_jobs!(runner)
jobs = RunnerJobsFinder.new(runner, params).execute
present paginate(jobs), with: Entities::JobBasicWithProject
end
end end
params do params do
...@@ -192,6 +209,12 @@ module API ...@@ -192,6 +209,12 @@ module API
forbidden!("No access granted") unless user_can_access_runner?(runner) forbidden!("No access granted") unless user_can_access_runner?(runner)
end end
def authenticate_list_runners_jobs!(runner)
return if current_user.admin?
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
def user_can_access_runner?(runner) def user_can_access_runner?(runner)
current_user.ci_authorized_runners.exists?(runner.id) current_user.ci_authorized_runners.exists?(runner.id)
end end
......
require 'spec_helper'
describe RunnerJobsFinder do
let(:project) { create(:project) }
let(:runner) { create(:ci_runner, :shared) }
subject { described_class.new(runner, params).execute }
describe '#execute' do
context 'when params is empty' do
let(:params) { {} }
let!(:job) { create(:ci_build, runner: runner, project: project) }
let!(:job1) { create(:ci_build, project: project) }
it 'returns all jobs assigned to Runner' do
is_expected.to match_array(job)
is_expected.not_to match_array(job1)
end
end
context 'when params contains status' do
HasStatus::AVAILABLE_STATUSES.each do |target_status|
context "when status is #{target_status}" do
let(:params) { { status: target_status } }
let!(:job) { create(:ci_build, runner: runner, project: project, status: target_status) }
before do
exception_status = HasStatus::AVAILABLE_STATUSES - [target_status]
create(:ci_build, runner: runner, project: project, status: exception_status.first)
end
it 'returns matched job' do
is_expected.to eq([job])
end
end
end
end
end
end
...@@ -11,7 +11,7 @@ describe('Sprite Icon Component', function () { ...@@ -11,7 +11,7 @@ describe('Sprite Icon Component', function () {
icon = mountComponent(IconComponent, { icon = mountComponent(IconComponent, {
name: 'test', name: 'test',
size: 99, size: 32,
cssClasses: 'extraclasses', cssClasses: 'extraclasses',
}); });
}); });
...@@ -34,12 +34,18 @@ describe('Sprite Icon Component', function () { ...@@ -34,12 +34,18 @@ describe('Sprite Icon Component', function () {
}); });
it('should properly compute iconSizeClass', function () { it('should properly compute iconSizeClass', function () {
expect(icon.iconSizeClass).toBe('s99'); expect(icon.iconSizeClass).toBe('s32');
});
it('forbids invalid size prop', () => {
expect(icon.$options.props.size.validator(NaN)).toBeFalsy();
expect(icon.$options.props.size.validator(0)).toBeFalsy();
expect(icon.$options.props.size.validator(9001)).toBeFalsy();
}); });
it('should properly render img css', function () { it('should properly render img css', function () {
const classList = icon.$el.classList; const classList = icon.$el.classList;
const containsSizeClass = classList.contains('s99'); const containsSizeClass = classList.contains('s32');
const containsCustomClass = classList.contains('extraclasses'); const containsCustomClass = classList.contains('extraclasses');
expect(containsSizeClass).toBe(true); expect(containsSizeClass).toBe(true);
expect(containsCustomClass).toBe(true); expect(containsCustomClass).toBe(true);
......
...@@ -354,6 +354,140 @@ describe API::Runners do ...@@ -354,6 +354,140 @@ describe API::Runners do
end end
end end
describe 'GET /runners/:id/jobs' do
set(:job_1) { create(:ci_build) }
let!(:job_2) { create(:ci_build, :running, runner: shared_runner, project: project) }
let!(:job_3) { create(:ci_build, :failed, runner: shared_runner, project: project) }
let!(:job_4) { create(:ci_build, :running, runner: specific_runner, project: project) }
let!(:job_5) { create(:ci_build, :failed, runner: specific_runner, project: project) }
context 'admin user' do
context 'when runner exists' do
context 'when runner is shared' do
it 'return jobs' do
get api("/runners/#{shared_runner.id}/jobs", admin)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(2)
end
end
context 'when runner is specific' do
it 'return jobs' do
get api("/runners/#{specific_runner.id}/jobs", admin)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(2)
end
end
context 'when valid status is provided' do
it 'return filtered jobs' do
get api("/runners/#{specific_runner.id}/jobs?status=failed", admin)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(1)
expect(json_response.first).to include('id' => job_5.id)
end
end
context 'when invalid status is provided' do
it 'return 400' do
get api("/runners/#{specific_runner.id}/jobs?status=non-existing", admin)
expect(response).to have_gitlab_http_status(400)
end
end
end
context "when runner doesn't exist" do
it 'returns 404' do
get api('/runners/9999/jobs', admin)
expect(response).to have_gitlab_http_status(404)
end
end
end
context "runner project's administrative user" do
context 'when runner exists' do
context 'when runner is shared' do
it 'returns 403' do
get api("/runners/#{shared_runner.id}/jobs", user)
expect(response).to have_gitlab_http_status(403)
end
end
context 'when runner is specific' do
it 'return jobs' do
get api("/runners/#{specific_runner.id}/jobs", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(2)
end
end
context 'when valid status is provided' do
it 'return filtered jobs' do
get api("/runners/#{specific_runner.id}/jobs?status=failed", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(1)
expect(json_response.first).to include('id' => job_5.id)
end
end
context 'when invalid status is provided' do
it 'return 400' do
get api("/runners/#{specific_runner.id}/jobs?status=non-existing", user)
expect(response).to have_gitlab_http_status(400)
end
end
end
context "when runner doesn't exist" do
it 'returns 404' do
get api('/runners/9999/jobs', user)
expect(response).to have_gitlab_http_status(404)
end
end
end
context 'other authorized user' do
it 'does not return jobs' do
get api("/runners/#{specific_runner.id}/jobs", user2)
expect(response).to have_gitlab_http_status(403)
end
end
context 'unauthorized user' do
it 'does not return jobs' do
get api("/runners/#{specific_runner.id}/jobs")
expect(response).to have_gitlab_http_status(401)
end
end
end
describe 'GET /projects/:id/runners' do describe 'GET /projects/:id/runners' do
context 'authorized user with master privileges' do context 'authorized user with master privileges' do
it "returns project's runners" do it "returns project's runners" do
......
require 'spec_helper'
describe Issuable::DestroyService do
let(:user) { create(:user) }
let(:project) { create(:project) }
subject(:service) { described_class.new(project, user) }
describe '#execute' do
context 'when issuable is an issue' do
let!(:issue) { create(:issue, project: project, author: user) }
it 'destroys the issue' do
expect { service.execute(issue) }.to change { project.issues.count }.by(-1)
end
it 'updates open issues count cache' do
expect_any_instance_of(Projects::OpenIssuesCountService).to receive(:refresh_cache)
service.execute(issue)
end
end
context 'when issuable is a merge request' do
let!(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: user) }
it 'destroys the merge request' do
expect { service.execute(merge_request) }.to change { project.merge_requests.count }.by(-1)
end
it 'updates open merge requests count cache' do
expect_any_instance_of(Projects::OpenMergeRequestsCountService).to receive(:refresh_cache)
service.execute(merge_request)
end
end
end
end
...@@ -105,8 +105,8 @@ describe StuckCiJobsWorker do ...@@ -105,8 +105,8 @@ describe StuckCiJobsWorker do
job.project.update(pending_delete: true) job.project.update(pending_delete: true)
end end
it 'does not drop job' do it 'does drop job' do
expect_any_instance_of(Ci::Build).not_to receive(:drop) expect_any_instance_of(Ci::Build).to receive(:drop).and_call_original
worker.perform worker.perform
end end
end end
...@@ -117,7 +117,7 @@ describe StuckCiJobsWorker do ...@@ -117,7 +117,7 @@ describe StuckCiJobsWorker do
let(:worker2) { described_class.new } let(:worker2) { described_class.new }
it 'is guard by exclusive lease when executed concurrently' do it 'is guard by exclusive lease when executed concurrently' do
expect(worker).to receive(:drop).at_least(:once) expect(worker).to receive(:drop).at_least(:once).and_call_original
expect(worker2).not_to receive(:drop) expect(worker2).not_to receive(:drop)
worker.perform worker.perform
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(false) allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(false)
...@@ -125,8 +125,8 @@ describe StuckCiJobsWorker do ...@@ -125,8 +125,8 @@ describe StuckCiJobsWorker do
end end
it 'can be executed in sequence' do it 'can be executed in sequence' do
expect(worker).to receive(:drop).at_least(:once) expect(worker).to receive(:drop).at_least(:once).and_call_original
expect(worker2).to receive(:drop).at_least(:once) expect(worker2).to receive(:drop).at_least(:once).and_call_original
worker.perform worker.perform
worker2.perform worker2.perform
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment