Commit f7783545 authored by Fabio Huser's avatar Fabio Huser Committed by Grzegorz Bizon

feat(merge-request): visualize coverage delta on MR widget

parent 050f57d9
...@@ -28,6 +28,10 @@ export default { ...@@ -28,6 +28,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
pipelineCoverageDelta: {
type: String,
required: false,
},
// This prop needs to be camelCase, html attributes are case insensive // This prop needs to be camelCase, html attributes are case insensive
// https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case // https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
hasCi: { hasCi: {
...@@ -92,6 +96,16 @@ export default { ...@@ -92,6 +96,16 @@ export default {
showSourceBranch() { showSourceBranch() {
return Boolean(this.pipeline.ref.branch); return Boolean(this.pipeline.ref.branch);
}, },
coverageDeltaClass() {
const delta = this.pipelineCoverageDelta;
if (delta && parseFloat(delta) > 0) {
return 'text-success';
}
if (delta && parseFloat(delta) < 0) {
return 'text-danger';
}
return '';
},
}, },
}; };
</script> </script>
...@@ -142,6 +156,14 @@ export default { ...@@ -142,6 +156,14 @@ export default {
</div> </div>
<div v-if="pipeline.coverage" class="coverage"> <div v-if="pipeline.coverage" class="coverage">
{{ s__('Pipeline|Coverage') }} {{ pipeline.coverage }}% {{ s__('Pipeline|Coverage') }} {{ pipeline.coverage }}%
<span
v-if="pipelineCoverageDelta"
class="js-pipeline-coverage-delta"
:class="coverageDeltaClass"
>
({{ pipelineCoverageDelta }}%)
</span>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -76,6 +76,7 @@ export default { ...@@ -76,6 +76,7 @@ export default {
<mr-widget-container> <mr-widget-container>
<mr-widget-pipeline <mr-widget-pipeline
:pipeline="pipeline" :pipeline="pipeline"
:pipeline-coverage-delta="mr.pipelineCoverageDelta"
:ci-status="mr.ciStatus" :ci-status="mr.ciStatus"
:has-ci="mr.hasCI" :has-ci="mr.hasCI"
:source-branch="branch" :source-branch="branch"
......
...@@ -42,6 +42,7 @@ export default class MergeRequestStore { ...@@ -42,6 +42,7 @@ export default class MergeRequestStore {
this.commitsCount = data.commits_count; this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count; this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {}; this.pipeline = data.pipeline || {};
this.pipelineCoverageDelta = data.pipeline_coverage_delta;
this.mergePipeline = data.merge_pipeline || {}; this.mergePipeline = data.merge_pipeline || {};
this.deployments = this.deployments || data.deployments || []; this.deployments = this.deployments || data.deployments || [];
this.postMergeDeployments = this.postMergeDeployments || []; this.postMergeDeployments = this.postMergeDeployments || [];
......
...@@ -1423,6 +1423,12 @@ class MergeRequest < ApplicationRecord ...@@ -1423,6 +1423,12 @@ class MergeRequest < ApplicationRecord
true true
end end
def pipeline_coverage_delta
if base_pipeline&.coverage && head_pipeline&.coverage
'%.2f' % (head_pipeline.coverage.to_f - base_pipeline.coverage.to_f)
end
end
def base_pipeline def base_pipeline
@base_pipeline ||= project.ci_pipelines @base_pipeline ||= project.ci_pipelines
.order(id: :desc) .order(id: :desc)
......
...@@ -57,6 +57,10 @@ class MergeRequestPollWidgetEntity < Grape::Entity ...@@ -57,6 +57,10 @@ class MergeRequestPollWidgetEntity < Grape::Entity
presenter(merge_request).ci_status presenter(merge_request).ci_status
end end
expose :pipeline_coverage_delta do |merge_request|
presenter(merge_request).pipeline_coverage_delta
end
expose :cancel_auto_merge_path do |merge_request| expose :cancel_auto_merge_path do |merge_request|
presenter(merge_request).cancel_auto_merge_path presenter(merge_request).cancel_auto_merge_path
end end
......
---
title: Add coverage difference visualization to merge request page
merge_request: 20676
author: Fabio Huser
type: added
...@@ -34,6 +34,7 @@ describe('MrWidgetPipelineContainer', () => { ...@@ -34,6 +34,7 @@ describe('MrWidgetPipelineContainer', () => {
expect(wrapper.find(MrWidgetPipeline).props()).toEqual( expect(wrapper.find(MrWidgetPipeline).props()).toEqual(
jasmine.objectContaining({ jasmine.objectContaining({
pipeline: mockStore.pipeline, pipeline: mockStore.pipeline,
pipelineCoverageDelta: mockStore.pipelineCoverageDelta,
ciStatus: mockStore.ciStatus, ciStatus: mockStore.ciStatus,
hasCi: mockStore.hasCI, hasCi: mockStore.hasCI,
sourceBranch: mockStore.sourceBranch, sourceBranch: mockStore.sourceBranch,
...@@ -68,6 +69,7 @@ describe('MrWidgetPipelineContainer', () => { ...@@ -68,6 +69,7 @@ describe('MrWidgetPipelineContainer', () => {
expect(wrapper.find(MrWidgetPipeline).props()).toEqual( expect(wrapper.find(MrWidgetPipeline).props()).toEqual(
jasmine.objectContaining({ jasmine.objectContaining({
pipeline: mockStore.mergePipeline, pipeline: mockStore.mergePipeline,
pipelineCoverageDelta: mockStore.pipelineCoverageDelta,
ciStatus: mockStore.ciStatus, ciStatus: mockStore.ciStatus,
hasCi: mockStore.hasCI, hasCi: mockStore.hasCI,
sourceBranch: mockStore.targetBranch, sourceBranch: mockStore.targetBranch,
......
...@@ -62,6 +62,38 @@ describe('MRWidgetPipeline', () => { ...@@ -62,6 +62,38 @@ describe('MRWidgetPipeline', () => {
expect(vm.hasCIError).toEqual(true); expect(vm.hasCIError).toEqual(true);
}); });
}); });
describe('coverageDeltaClass', () => {
it('should return no class if there is no coverage change', () => {
vm = mountComponent(Component, {
pipeline: mockData.pipeline,
pipelineCoverageDelta: '0',
troubleshootingDocsPath: 'help',
});
expect(vm.coverageDeltaClass).toEqual('');
});
it('should return text-success if the coverage increased', () => {
vm = mountComponent(Component, {
pipeline: mockData.pipeline,
pipelineCoverageDelta: '10',
troubleshootingDocsPath: 'help',
});
expect(vm.coverageDeltaClass).toEqual('text-success');
});
it('should return text-danger if the coverage decreased', () => {
vm = mountComponent(Component, {
pipeline: mockData.pipeline,
pipelineCoverageDelta: '-12',
troubleshootingDocsPath: 'help',
});
expect(vm.coverageDeltaClass).toEqual('text-danger');
});
});
}); });
describe('rendered output', () => { describe('rendered output', () => {
...@@ -96,6 +128,7 @@ describe('MRWidgetPipeline', () => { ...@@ -96,6 +128,7 @@ describe('MRWidgetPipeline', () => {
pipeline: mockData.pipeline, pipeline: mockData.pipeline,
hasCi: true, hasCi: true,
ciStatus: 'success', ciStatus: 'success',
pipelineCoverageDelta: mockData.pipelineCoverageDelta,
troubleshootingDocsPath: 'help', troubleshootingDocsPath: 'help',
}); });
}); });
...@@ -132,6 +165,13 @@ describe('MRWidgetPipeline', () => { ...@@ -132,6 +165,13 @@ describe('MRWidgetPipeline', () => {
`Coverage ${mockData.pipeline.coverage}`, `Coverage ${mockData.pipeline.coverage}`,
); );
}); });
it('should render pipeline coverage delta information', () => {
expect(vm.$el.querySelector('.js-pipeline-coverage-delta.text-danger')).toBeDefined();
expect(vm.$el.querySelector('.js-pipeline-coverage-delta').textContent).toContain(
`(${mockData.pipelineCoverageDelta}%)`,
);
});
}); });
describe('without commit path', () => { describe('without commit path', () => {
......
...@@ -185,6 +185,7 @@ export default { ...@@ -185,6 +185,7 @@ export default {
created_at: '2017-04-07T12:27:19.520Z', created_at: '2017-04-07T12:27:19.520Z',
updated_at: '2017-04-07T15:28:44.800Z', updated_at: '2017-04-07T15:28:44.800Z',
}, },
pipelineCoverageDelta: '15.25',
work_in_progress: false, work_in_progress: false,
source_branch_exists: false, source_branch_exists: false,
mergeable_discussions_state: true, mergeable_discussions_state: true,
......
...@@ -2821,6 +2821,63 @@ describe MergeRequest do ...@@ -2821,6 +2821,63 @@ describe MergeRequest do
end end
end end
describe '#pipeline_coverage_delta' do
let!(:project) { create(:project, :repository) }
let!(:merge_request) { create(:merge_request, source_project: project) }
let!(:source_pipeline) do
create(:ci_pipeline,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha
)
end
let!(:target_pipeline) do
create(:ci_pipeline,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha
)
end
def create_build(pipeline, coverage, name)
create(:ci_build, :success, pipeline: pipeline, coverage: coverage, name: name)
merge_request.update_head_pipeline
end
context 'when both source and target branches have coverage information' do
it 'returns the appropriate coverage delta' do
create_build(source_pipeline, 60.2, 'test:1')
create_build(target_pipeline, 50, 'test:2')
expect(merge_request.pipeline_coverage_delta).to eq('10.20')
end
end
context 'when target branch does not have coverage information' do
it 'returns nil' do
create_build(source_pipeline, 50, 'test:1')
expect(merge_request.pipeline_coverage_delta).to be_nil
end
end
context 'when source branch does not have coverage information' do
it 'returns nil for coverage_delta' do
create_build(target_pipeline, 50, 'test:1')
expect(merge_request.pipeline_coverage_delta).to be_nil
end
end
context 'neither source nor target branch has coverage information' do
it 'returns nil for coverage_delta' do
expect(merge_request.pipeline_coverage_delta).to be_nil
end
end
end
describe '#base_pipeline' do describe '#base_pipeline' do
let(:pipeline_arguments) do let(:pipeline_arguments) do
{ {
......
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