Commit eeb7245f authored by Sean McGivern's avatar Sean McGivern

Merge branch 'dz-codeclimate-compare-ee' into 'master'

Compare codeclimate artifacts on the merge request page

See merge request !1984
parents 5115062d 4fc52e1c
<script>
import successIcon from 'icons/_icon_status_success.svg';
import errorIcon from 'icons/_icon_status_failed.svg';
import issuesBlock from './mr_widget_code_quality_issues.vue';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import '../../../lib/utils/text_utility';
export default {
name: 'MRWidgetCodeQuality',
props: {
mr: {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
},
components: {
issuesBlock,
loadingIcon,
},
data() {
return {
collapseText: 'Expand',
isCollapsed: true,
isLoading: false,
loadingFailed: false,
};
},
computed: {
stateIcon() {
return this.mr.codeclimateMetrics.newIssues.length ? errorIcon : successIcon;
},
hasNoneIssues() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
return !newIssues.length && !resolvedIssues.length;
},
hasIssues() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
return newIssues.length || resolvedIssues.length;
},
codeText() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
let newIssuesText = '';
let resolvedIssuesText = '';
let text = '';
if (this.hasNoneIssues) {
text = 'No changes to code quality so far.';
} else if (this.hasIssues) {
if (newIssues.length) {
newIssuesText = `degraded on ${newIssues.length} ${this.pointsText(newIssues)}`;
}
if (resolvedIssues.length) {
resolvedIssuesText = `improved on ${resolvedIssues.length} ${this.pointsText(resolvedIssues)}`;
}
const connector = this.hasIssues ? 'and' : '';
text = `Code quality ${resolvedIssuesText} ${connector} ${newIssuesText}.`;
}
return text;
},
},
methods: {
pointsText(issues) {
return gl.text.pluralize('point', issues.length);
},
toggleCollapsed() {
this.isCollapsed = !this.isCollapsed;
const text = this.isCollapsed ? 'Expand' : 'Collapse';
this.collapseText = text;
},
handleError() {
this.isLoading = false;
this.loadingFailed = true;
},
},
created() {
const { head_path, base_path } = this.mr.codeclimate;
this.isLoading = true;
this.service.fetchCodeclimate(head_path)
.then(resp => resp.json())
.then((data) => {
this.mr.setCodeclimateHeadMetrics(data);
this.service.fetchCodeclimate(base_path)
.then(response => response.json())
.then(baseData => this.mr.setCodeclimateBaseMetrics(baseData))
.then(() => this.mr.compareCodeclimateMetrics())
.then(() => {
this.isLoading = false;
})
.catch(() => this.handleError());
})
.catch(() => this.handleError());
},
};
</script>
<template>
<section class="mr-widget-code-quality">
<div
v-if="isLoading"
class="padding-left">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true">
</i>
Loading codeclimate report.
</div>
<div v-else-if="!isLoading && !loadingFailed">
<span
class="padding-left ci-status-icon"
:class="{
'ci-status-icon-failed': mr.codeclimateMetrics.newIssues.length,
'ci-status-icon-passed': mr.codeclimateMetrics.newIssues.length === 0
}"
v-html="stateIcon">
</span>
<span>
{{codeText}}
</span>
<button
type="button"
class="btn-link btn-blank"
v-if="hasIssues"
@click="toggleCollapsed">
{{collapseText}}
</button>
<div
class="code-quality-container"
v-if="hasIssues"
v-show="!isCollapsed">
<issues-block
class="js-mr-code-resolved-issues"
v-if="mr.codeclimateMetrics.resolvedIssues.length"
type="success"
:issues="mr.codeclimateMetrics.resolvedIssues"
/>
<issues-block
class="js-mr-code-new-issues"
v-if="mr.codeclimateMetrics.newIssues.length"
type="failed"
:issues="mr.codeclimateMetrics.newIssues"
/>
</div>
</div>
<div
v-else-if="loadingFailed"
class="padding-left">
Failed to load codeclimate report.
</div>
</section>
</template>
<script>
export default {
name: 'MRWidgetCodeQualityIssues',
props: {
issues: {
type: Array,
required: true,
},
type: {
type: String,
required: true,
},
},
};
</script>
<template>
<ul class="mr-widget-code-quality-list">
<li
class="commit-sha"
:class="{
failed: type === 'failed',
success: type === 'success'
}
"v-for="issue in issues">
<i
class="fa"
:class="{
'fa-minus': type === 'failed',
'fa-plus': type === 'success'
}"
aria-hidden="true">
</i>
<span>
<span v-if="type === 'success'">Fixed:</span>
{{issue.check_name}}
{{issue.location.path}}
{{issue.location.positions}}
{{issue.location.lines}}
</span>
</li>
</ul>
</template>
......@@ -2,6 +2,7 @@ import CEWidgetOptions from '../mr_widget_options';
import WidgetApprovals from './components/approvals/mr_widget_approvals';
import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node';
import RebaseState from './components/states/mr_widget_rebase';
import WidgetCodeQuality from './components/mr_widget_code_quality.vue';
export default {
extends: CEWidgetOptions,
......@@ -9,11 +10,16 @@ export default {
'mr-widget-approvals': WidgetApprovals,
'mr-widget-geo-secondary-node': GeoSecondaryNode,
'mr-widget-rebase': RebaseState,
'mr-widget-code-quality': WidgetCodeQuality,
},
computed: {
shouldRenderApprovals() {
return this.mr.approvalsRequired;
},
shouldRenderCodeQuality() {
const { codeclimate } = this.mr;
return codeclimate && codeclimate.head_path && codeclimate.base_path;
},
},
template: `
<div class="mr-state-widget prepend-top-default">
......@@ -29,6 +35,11 @@ export default {
v-if="mr.approvalsRequired"
:mr="mr"
:service="service" />
<mr-widget-code-quality
v-if="shouldRenderCodeQuality"
:mr="mr"
:service="service"
/>
<component
:is="componentName"
:mr="mr"
......
......@@ -28,4 +28,8 @@ export default class MRWidgetService extends CEWidgetService {
rebase() {
return this.rebaseResource.save();
}
fetchCodeclimate(endpoint) { // eslint-disable-line
return Vue.http.get(endpoint);
}
}
import CEMergeRequestStore from '../../stores/mr_widget_store';
export default class MergeRequestStore extends CEMergeRequestStore {
constructor(data) {
super(data);
this.initCodeclimate(data);
}
setData(data) {
this.initGeo(data);
this.initSquashBeforeMerge(data);
......@@ -43,4 +48,33 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.isApproved = !this.approvalsLeft || false;
this.preventMerge = this.approvalsRequired && this.approvalsLeft;
}
initCodeclimate(data) {
this.codeclimate = data.codeclimate;
this.codeclimateMetrics = {
headIssues: [],
baseIssues: [],
newIssues: [],
resolvedIssues: [],
};
}
setCodeclimateHeadMetrics(data) {
this.codeclimateMetrics.headIssues = data;
}
setCodeclimateBaseMetrics(data) {
this.codeclimateMetrics.baseIssues = data;
}
compareCodeclimateMetrics() {
const { headIssues, baseIssues } = this.codeclimateMetrics;
this.codeclimateMetrics.newIssues = this.filterByFingerprint(headIssues, baseIssues);
this.codeclimateMetrics.resolvedIssues = this.filterByFingerprint(baseIssues, headIssues);
}
filterByFingerprint(firstArray, secondArray) { // eslint-disable-line
return firstArray.filter(item => !secondArray.find(el => el.fingerprint === item.fingerprint));
}
}
......@@ -81,13 +81,12 @@ export default {
.then((res) => {
this.mr.setData(res);
this.setFavicon();
if (cb) {
cb.call(null, res);
}
})
.catch(() => {
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
.catch(() => new Flash('Something went wrong. Please try again.'));
},
initPolling() {
this.pollingInterval = new gl.SmartInterval({
......@@ -134,9 +133,7 @@ export default {
document.body.appendChild(el);
}
})
.catch(() => {
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
.catch(() => new Flash('Something went wrong. Please try again.'));
},
resumePolling() {
this.pollingInterval.resume();
......
......@@ -2,9 +2,9 @@ import Timeago from 'timeago.js';
import { getStateKey } from '../dependencies';
export default class MergeRequestStore {
constructor(data) {
this.sha = data.diff_head_sha;
this.setData(data);
}
......@@ -135,5 +135,4 @@ export default class MergeRequestStore {
return timeagoInstance.format(event.updated_at);
}
}
......@@ -875,3 +875,42 @@
}
}
}
.mr-widget-code-quality {
padding-top: $gl-padding-top;
.padding-left {
padding-left: $gl-padding;
}
.ci-status-icon {
vertical-align: sub;
svg {
width: 22px;
height: 22px;
margin-right: 4px;
}
}
.code-quality-container {
border-top: 1px solid $gray-darker;
border-bottom: 1px solid $gray-darker;
padding: $gl-padding-top;
background-color: $gray-light;
.mr-widget-code-quality-list {
list-style: none;
padding: 0 36px;
margin: 0;
li.success {
color: $green-500;
}
li.failed {
color: $red-500;
}
}
}
}
......@@ -35,6 +35,7 @@ module Ci
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual).relevant }
scope :codeclimate, ->() { where(name: 'codeclimate') }
mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader
......@@ -446,6 +447,11 @@ module Ci
trace
end
def has_codeclimate_json?
options.dig(:artifacts, :paths) == ['codeclimate.json'] &&
artifacts_metadata?
end
private
def update_artifacts_size
......
......@@ -396,6 +396,10 @@ module Ci
.fabricate!
end
def codeclimate_artifact
artifacts.codeclimate.find(&:has_codeclimate_json?)
end
private
def pipeline_data
......
......@@ -34,6 +34,9 @@ class MergeRequest < ActiveRecord::Base
delegate :commits, :real_size, :commits_sha, :commits_count,
to: :merge_request_diff, prefix: nil
delegate :codeclimate_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
delegate :codeclimate_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
attr_accessor :allow_broken
......@@ -971,4 +974,13 @@ class MergeRequest < ActiveRecord::Base
true
end
def base_pipeline
@base_pipeline ||= project.pipelines.find_by(sha: merge_request_diff&.base_commit_sha)
end
def has_codeclimate_data?
!!(head_codeclimate_artifact&.success? &&
base_codeclimate_artifact&.success?)
end
end
......@@ -197,6 +197,23 @@ class MergeRequestEntity < IssuableEntity
merge_request)
end
# EE-specific
expose :codeclimate, if: -> (mr, _) { mr.has_codeclimate_data? } do
expose :head_path, if: -> (mr, _) { can?(current_user, :read_build, mr.head_codeclimate_artifact) } do |merge_request|
raw_namespace_project_build_artifacts_url(merge_request.source_project.namespace,
merge_request.source_project,
merge_request.head_codeclimate_artifact,
path: 'codeclimate.json')
end
expose :base_path, if: -> (mr, _) { can?(current_user, :read_build, mr.base_codeclimate_artifact) } do |merge_request|
raw_namespace_project_build_artifacts_url(merge_request.target_project.namespace,
merge_request.target_project,
merge_request.base_codeclimate_artifact,
path: 'codeclimate.json')
end
end
private
delegate :current_user, to: :request
......
---
title: Compare codeclimate artifacts on the merge request page
merge_request: 1984
author:
# Analyze project code quality with Code Climate CLI
This example shows how to run [Code Climate CLI][cli] on your code by using\
This example shows how to run [Code Climate CLI][cli] on your code by using
GitLab CI and Docker.
First, you need GitLab Runner with [docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor).
First, you need GitLab Runner with [docker-in-docker executor][dind].
Once you setup the Runner add new job to `.gitlab-ci.yml`:
Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `codeclimate`:
```yaml
codeclimate:
......@@ -25,4 +25,10 @@ codeclimate:
This will create a `codeclimate` job in your CI pipeline and will allow you to
download and analyze the report artifact in JSON format.
For GitLab [Enterprise Edition Starter][ee] users, this information can be automatically
extracted and shown right in the merge request widget. [Learn more on code quality
diffs in merge requests](../../user/project/merge_requests/code_quality_diff.md).
[cli]: https://github.com/codeclimate/codeclimate
[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[ee]: https://about.gitlab.com/gitlab-ee/
# Code quality diff using Code Climate
> [Introduced][ee-1984] in [GitLab Enterprise Edition Starter][ee] 9.3.
If you are using [GitLab CI][ci], you can analyze your source code quality using
the [Code Climate][cc] analyzer [Docker image][cd]. Going a step further, GitLab
can show the Code Climate report right in the merge request widget area.
![Code Quality Widget][quality-widget]
In order for the report to show in the merge request, you need to specify a
`codeclimate` job (exact name) that will analyze the code and upload the resulting
`codeclimate.json` as an artifact. GitLab will then check this file and show
the information inside the merge request.
For more information on how the `codeclimate` job should look like, check the
example on [analyzing a project's code quality with Code Climate CLI][cc-docs].
[ee-1984]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1984
[ee]: https://about.gitlab.com/gitlab-ee/
[ci]: ../../../ci/README.md
[cc]: https://codeclimate.com
[cd]: https://hub.docker.com/r/codeclimate/codeclimate/
[quality-widget]: img/code_quality.gif
[cc-docs]: ../../../ci/examples/code_climate.md
......@@ -105,6 +105,16 @@ creating merge commits, you can configure this on a per-project basis.
[Read more about fast-forward merge requests.](fast_forward_merge.md)
## Code quality diff
> Introduced in [GitLab Enterprise Edition Starter][products] 9.3.
If you are using [GitLab CI][ci], you can analyze your source code quality using
the [Code Climate][cc] analyzer [Docker image][cd]. Going a step further, GitLab
can show the Code Climate report right in the merge request widget area.
[Read more about code quality diff.](code_quality_diff.md)
## Ignore whitespace changes in Merge Request diff view
If you click the **Hide whitespace changes** button, you can see the diff
......@@ -206,3 +216,6 @@ git checkout origin/merge-requests/1
[protected branches]: ../protected_branches.md
[products]: https://about.gitlab.com/products/ "GitLab products page"
[ci]: ../../../ci/README.md
[cc]: https://codeclimate.com/
[cd]: https://hub.docker.com/r/codeclimate/codeclimate/
......@@ -104,7 +104,11 @@
"rebase_path": { "type": ["string", "null"] },
"approved": { "type": "boolean" },
"approvals_path": { "type": ["string", "null"] },
"ff_only_enabled": { "type": "boolean" }
"ff_only_enabled": { "type": "boolean" },
"codeclimate": {
"head_path": { "type": "string" },
"base_path": { "type": "string" }
}
},
"additionalProperties": false
}
import Vue from 'vue';
import mrWidgetCodeQualityIssues from '~/vue_merge_request_widget/ee/components/mr_widget_code_quality_issues.vue';
describe('Merge Request Code Quality Issues', () => {
let vm;
let MRWidgetCodeQualityIssues;
let mountComponent;
beforeEach(() => {
MRWidgetCodeQualityIssues = Vue.extend(mrWidgetCodeQualityIssues);
mountComponent = props => new MRWidgetCodeQualityIssues({ propsData: props }).$mount();
});
describe('Renders provided list of issues', () => {
describe('with positions and lines', () => {
beforeEach(() => {
vm = mountComponent({
type: 'success',
issues: [{
check_name: 'foo',
location: {
path: 'bar',
positions: '81',
lines: '21',
},
}],
});
});
it('should render issue', () => {
expect(
vm.$el.querySelector('li span').textContent.trim().replace(/\s+/g, ''),
).toEqual('Fixed:foobar8121');
});
});
describe('without positions and lines', () => {
beforeEach(() => {
vm = mountComponent({
type: 'success',
issues: [{
check_name: 'foo',
location: {
path: 'bar',
},
}],
});
});
it('should render issue without position and lines', () => {
expect(
vm.$el.querySelector('li span').textContent.trim().replace(/\s+/g, ''),
).toEqual('Fixed:foobar');
});
});
describe('for type failed', () => {
beforeEach(() => {
vm = mountComponent({
type: 'failed',
issues: [{
check_name: 'foo',
location: {
path: 'bar',
positions: '81',
lines: '21',
},
}],
});
});
it('should render failed minus icon', () => {
expect(vm.$el.querySelector('li').classList.contains('failed')).toEqual(true);
expect(vm.$el.querySelector('li i').classList.contains('fa-minus')).toEqual(true);
});
});
describe('for type success', () => {
beforeEach(() => {
vm = mountComponent({
type: 'success',
issues: [{
check_name: 'foo',
location: {
path: 'bar',
positions: '81',
lines: '21',
},
}],
});
});
it('should render success plus icon', () => {
expect(vm.$el.querySelector('li').classList.contains('success')).toEqual(true);
expect(vm.$el.querySelector('li i').classList.contains('fa-plus')).toEqual(true);
});
});
});
});
import Vue from 'vue';
import mrWidgetCodeQuality from '~/vue_merge_request_widget/ee/components/mr_widget_code_quality.vue';
import Store from '~/vue_merge_request_widget/ee/stores/mr_widget_store';
import Service from '~/vue_merge_request_widget/ee/services/mr_widget_service';
import mockData, { baseIssues, headIssues } from '../mock_data';
describe('Merge Request Code Quality', () => {
let vm;
let MRWidgetCodeQuality;
let store;
let mountComponent;
let service;
beforeEach(() => {
MRWidgetCodeQuality = Vue.extend(mrWidgetCodeQuality);
store = new Store(mockData);
service = new Service('');
mountComponent = props => new MRWidgetCodeQuality({ propsData: props }).$mount();
});
afterEach(() => {
vm.$destroy();
});
describe('when it is loading', () => {
beforeEach(() => {
vm = mountComponent({
mr: store,
service,
});
});
it('should render loading indicator', () => {
expect(vm.$el.textContent.trim()).toEqual('Loading codeclimate report.');
});
});
describe('with successfull request', () => {
const interceptor = (request, next) => {
if (request.url === 'head.json') {
next(request.respondWith(JSON.stringify(headIssues), {
status: 200,
}));
}
if (request.url === 'base.json') {
next(request.respondWith(JSON.stringify(baseIssues), {
status: 200,
}));
}
};
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
vm = mountComponent({
mr: store,
service,
});
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('should render provided data', (done) => {
setTimeout(() => {
expect(
vm.$el.querySelector('span:nth-child(2)').textContent.trim(),
).toEqual('Code quality improved on 1 point and degraded on 1 point.');
done();
}, 0);
});
describe('toggleCollapsed', () => {
it('toggles issues', (done) => {
setTimeout(() => {
vm.$el.querySelector('button').click();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.code-quality-container').geAttribute('style'),
).toEqual(null);
expect(
vm.$el.querySelector('button').textContent.trim(),
).toEqual('Collapse');
vm.$el.querySelector('button').click();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.code-quality-container').geAttribute('style'),
).toEqual('display: none;');
expect(
vm.$el.querySelector('button').textContent.trim(),
).toEqual('Expand');
});
});
done();
}, 0);
});
});
});
describe('with empty successfull request', () => {
const emptyInterceptor = (request, next) => {
if (request.url === 'head.json') {
next(request.respondWith(JSON.stringify([]), {
status: 200,
}));
}
if (request.url === 'base.json') {
next(request.respondWith(JSON.stringify([]), {
status: 200,
}));
}
};
beforeEach(() => {
Vue.http.interceptors.push(emptyInterceptor);
vm = mountComponent({
mr: store,
service,
});
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, emptyInterceptor);
});
it('should render provided data', (done) => {
setTimeout(() => {
expect(
vm.$el.querySelector('span:nth-child(2)').textContent.trim(),
).toEqual('No changes to code quality so far.');
done();
}, 0);
});
});
describe('with failed request', () => {
const errorInterceptor = (request, next) => {
if (request.url === 'head.json') {
next(request.respondWith(JSON.stringify([]), {
status: 500,
}));
}
if (request.url === 'base.json') {
next(request.respondWith(JSON.stringify([]), {
status: 500,
}));
}
};
beforeEach(() => {
Vue.http.interceptors.push(errorInterceptor);
vm = mountComponent({
mr: store,
service,
});
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor);
});
it('should render error indicator', (done) => {
setTimeout(() => {
expect(vm.$el.textContent.trim()).toEqual('Failed to load codeclimate report.');
done();
}, 0);
});
});
});
......@@ -210,5 +210,68 @@ export default {
"merge_commit_message_with_description": "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
"diverged_commits_count": 0,
"only_allow_merge_if_pipeline_succeeds": false,
"commit_change_content_path": "/root/acets-app/merge_requests/22/commit_change_content"
}
"commit_change_content_path": "/root/acets-app/merge_requests/22/commit_change_content",
"codeclimate": {
"head_path": "head.json",
"base_path": "base.json"
}
};
export const headIssues = [
{
"check_name": "Rubocop/Lint/UselessAssignment",
"location": {
"path": "lib/six.rb",
"positions": {
"begin": {
"column": 6,
"line": 59
},
"end": {
"column": 7,
"line": 59
}
}
},
"fingerprint": "e879dd9bbc0953cad5037cde7ff0f627",
},
{
"categories": ["Security"],
"check_name": "Insecure Dependency",
"location": {
"path": "Gemfile.lock",
"lines": {
"begin": 22,
"end": 22
}
},
"fingerprint": "ca2e59451e98ae60ba2f54e3857c50e5",
}
];
export const baseIssues = [
{
"categories": ["Security"],
"check_name": "Insecure Dependency",
"location": {
"path": "Gemfile.lock",
"lines": {
"begin": 22,
"end": 22
}
},
"fingerprint": "ca2e59451e98ae60ba2f54e3857c50e5",
},
{
"categories": ["Security"],
"check_name": "Insecure Dependency",
"location": {
"path": "Gemfile.lock",
"lines": {
"begin": 21,
"end": 21
}
},
"fingerprint": "ca2354534dee94ae60ba2f54e3857c50e5",
}
]
import MergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
import mockData from '../mock_data';
import MergeRequestStore from '~/vue_merge_request_widget/ee/stores/mr_widget_store';
import mockData, { headIssues, baseIssues } from '../mock_data';
describe('MergeRequestStore', () => {
describe('setData', () => {
let store;
let store;
beforeEach(() => {
store = new MergeRequestStore(mockData);
});
beforeEach(() => {
store = new MergeRequestStore(mockData);
});
describe('setData', () => {
it('should set hasSHAChanged when the diff SHA changes', () => {
store.setData({ ...mockData, diff_head_sha: 'a-different-string' });
expect(store.hasSHAChanged).toBe(true);
......@@ -19,4 +19,45 @@ describe('MergeRequestStore', () => {
expect(store.hasSHAChanged).toBe(false);
});
});
describe('setCodeclimateHeadMetrics', () => {
it('should set defaults', () => {
expect(store.codeclimate).toEqual(mockData.codeclimate);
expect(store.codeclimateMetrics).toEqual({
headIssues: [],
baseIssues: [],
newIssues: [],
resolvedIssues: [],
});
});
it('should set the provided head metrics', () => {
store.setCodeclimateHeadMetrics(headIssues);
expect(store.codeclimateMetrics.headIssues).toEqual(headIssues);
});
});
describe('setCodeclimateBaseMetrics', () => {
it('should set the provided base metrics', () => {
store.setCodeclimateBaseMetrics(baseIssues);
expect(store.codeclimateMetrics.baseIssues).toEqual(baseIssues);
});
});
describe('compareCodeclimateMetrics', () => {
beforeEach(() => {
store.setCodeclimateHeadMetrics(headIssues);
store.setCodeclimateBaseMetrics(baseIssues);
store.compareCodeclimateMetrics();
});
it('should return the new issues', () => {
expect(store.codeclimateMetrics.newIssues[0]).toEqual(headIssues[0]);
});
it('should return the resolved issues', () => {
expect(store.codeclimateMetrics.resolvedIssues[0]).toEqual(baseIssues[1]);
});
});
});
......@@ -1367,4 +1367,38 @@ describe Ci::Build, :models do
build.enqueue
end
end
describe '#has_codeclimate_json?' do
context 'valid build' do
let!(:build) do
create(
:ci_build,
:artifacts,
name: 'codeclimate',
pipeline: pipeline,
options: {
artifacts: {
paths: ['codeclimate.json']
}
}
)
end
it { expect(build.has_codeclimate_json?).to be_truthy }
end
context 'invalid build' do
let!(:build) do
create(
:ci_build,
:artifacts,
name: 'codeclimate',
pipeline: pipeline,
options: {}
)
end
it { expect(build.has_codeclimate_json?).to be_falsey }
end
end
end
......@@ -1218,4 +1218,30 @@ describe Ci::Pipeline, models: true do
it_behaves_like 'not sending any notification'
end
end
describe '#codeclimate_artifact' do
context 'has codeclimate build' do
let!(:build) do
create(
:ci_build,
:artifacts,
name: 'codeclimate',
pipeline: pipeline,
options: {
artifacts: {
paths: ['codeclimate.json']
}
}
)
end
it { expect(pipeline.codeclimate_artifact).to eq(build) }
end
context 'no codeclimate build' do
before { create(:ci_build, pipeline: pipeline) }
it { expect(pipeline.codeclimate_artifact).to be_nil }
end
end
end
......@@ -2065,4 +2065,48 @@ describe MergeRequest, models: true do
end
end
end
describe '#base_pipeline' do
let!(:pipeline) { create(:ci_empty_pipeline, project: subject.project, sha: subject.diff_base_sha) }
it { expect(subject.base_pipeline).to eq(pipeline) }
end
describe '#base_codeclimate_artifact' do
before do
allow(subject.base_pipeline).to receive(:codeclimate_artifact).
and_return(1)
end
it 'delegates to merge request diff' do
expect(subject.base_codeclimate_artifact).to eq(1)
end
end
describe '#head_codeclimate_artifact' do
before do
allow(subject.head_pipeline).to receive(:codeclimate_artifact).
and_return(1)
end
it 'delegates to merge request diff' do
expect(subject.head_codeclimate_artifact).to eq(1)
end
end
describe '#has_codeclimate_data?' do
context 'with codeclimate artifact' do
before do
artifact = double(success?: true)
allow(subject.head_pipeline).to receive(:codeclimate_artifact).and_return(artifact)
allow(subject.base_pipeline).to receive(:codeclimate_artifact).and_return(artifact)
end
it { expect(subject.has_codeclimate_data?).to be_truthy }
end
context 'without codeclimate artifact' do
it { expect(subject.has_codeclimate_data?).to be_falsey }
end
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