Commit fb56d1ef authored by Scott Hampton's avatar Scott Hampton

Merge branch '271077-move-codequality-diff-out-of-frontend-step-2' into 'master'

Use backend code quality diff for MR widget when feature flag is enabled

See merge request gitlab-org/gitlab!52365
parents 056e4ed0 fb429646
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { mapState, mapActions, mapGetters } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import { componentNames } from '~/reports/components/issue_body'; import { componentNames } from '~/reports/components/issue_body';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportSection from '~/reports/components/report_section.vue'; import ReportSection from '~/reports/components/report_section.vue';
import createStore from './store'; import createStore from './store';
...@@ -11,6 +12,7 @@ export default { ...@@ -11,6 +12,7 @@ export default {
components: { components: {
ReportSection, ReportSection,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
headPath: { headPath: {
type: String, type: String,
...@@ -30,6 +32,11 @@ export default { ...@@ -30,6 +32,11 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
codequalityReportsPath: {
type: String,
required: false,
default: '',
},
codequalityHelpPath: { codequalityHelpPath: {
type: String, type: String,
required: true, required: true,
...@@ -37,7 +44,7 @@ export default { ...@@ -37,7 +44,7 @@ export default {
}, },
componentNames, componentNames,
computed: { computed: {
...mapState(['newIssues', 'resolvedIssues']), ...mapState(['newIssues', 'resolvedIssues', 'hasError', 'statusReason']),
...mapGetters([ ...mapGetters([
'hasCodequalityIssues', 'hasCodequalityIssues',
'codequalityStatus', 'codequalityStatus',
...@@ -51,10 +58,11 @@ export default { ...@@ -51,10 +58,11 @@ export default {
headPath: this.headPath, headPath: this.headPath,
baseBlobPath: this.baseBlobPath, baseBlobPath: this.baseBlobPath,
headBlobPath: this.headBlobPath, headBlobPath: this.headBlobPath,
reportsPath: this.codequalityReportsPath,
helpPath: this.codequalityHelpPath, helpPath: this.codequalityHelpPath,
}); });
this.fetchReports(); this.fetchReports(this.glFeatures.codequalityMrDiff);
}, },
methods: { methods: {
...mapActions(['fetchReports', 'setPaths']), ...mapActions(['fetchReports', 'setPaths']),
...@@ -80,5 +88,7 @@ export default { ...@@ -80,5 +88,7 @@ export default {
:popover-options="codequalityPopover" :popover-options="codequalityPopover"
:show-report-section-status-icon="false" :show-report-section-status-icon="false"
class="js-codequality-widget mr-widget-border-top mr-report" class="js-codequality-widget mr-widget-border-top mr-report"
/> >
<template v-if="hasError" #sub-heading>{{ statusReason }}</template>
</report-section>
</template> </template>
...@@ -4,9 +4,20 @@ import { parseCodeclimateMetrics, doCodeClimateComparison } from './utils/codequ ...@@ -4,9 +4,20 @@ import { parseCodeclimateMetrics, doCodeClimateComparison } from './utils/codequ
export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths); export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths);
export const fetchReports = ({ state, dispatch, commit }) => { export const fetchReports = ({ state, dispatch, commit }, diffFeatureFlagEnabled) => {
commit(types.REQUEST_REPORTS); commit(types.REQUEST_REPORTS);
if (diffFeatureFlagEnabled) {
return axios
.get(state.reportsPath)
.then(({ data }) => {
return dispatch('receiveReportsSuccess', {
newIssues: parseCodeclimateMetrics(data.new_errors, state.headBlobPath),
resolvedIssues: parseCodeclimateMetrics(data.resolved_errors, state.baseBlobPath),
});
})
.catch((error) => dispatch('receiveReportsError', error));
}
if (!state.basePath) { if (!state.basePath) {
return dispatch('receiveReportsError'); return dispatch('receiveReportsError');
} }
...@@ -18,13 +29,13 @@ export const fetchReports = ({ state, dispatch, commit }) => { ...@@ -18,13 +29,13 @@ export const fetchReports = ({ state, dispatch, commit }) => {
), ),
) )
.then((data) => dispatch('receiveReportsSuccess', data)) .then((data) => dispatch('receiveReportsSuccess', data))
.catch(() => dispatch('receiveReportsError')); .catch((error) => dispatch('receiveReportsError', error));
}; };
export const receiveReportsSuccess = ({ commit }, data) => { export const receiveReportsSuccess = ({ commit }, data) => {
commit(types.RECEIVE_REPORTS_SUCCESS, data); commit(types.RECEIVE_REPORTS_SUCCESS, data);
}; };
export const receiveReportsError = ({ commit }) => { export const receiveReportsError = ({ commit }, error) => {
commit(types.RECEIVE_REPORTS_ERROR); commit(types.RECEIVE_REPORTS_ERROR, error);
}; };
...@@ -6,6 +6,7 @@ export default { ...@@ -6,6 +6,7 @@ export default {
state.headPath = paths.headPath; state.headPath = paths.headPath;
state.baseBlobPath = paths.baseBlobPath; state.baseBlobPath = paths.baseBlobPath;
state.headBlobPath = paths.headBlobPath; state.headBlobPath = paths.headBlobPath;
state.reportsPath = paths.reportsPath;
state.helpPath = paths.helpPath; state.helpPath = paths.helpPath;
}, },
[types.REQUEST_REPORTS](state) { [types.REQUEST_REPORTS](state) {
...@@ -13,12 +14,14 @@ export default { ...@@ -13,12 +14,14 @@ export default {
}, },
[types.RECEIVE_REPORTS_SUCCESS](state, data) { [types.RECEIVE_REPORTS_SUCCESS](state, data) {
state.hasError = false; state.hasError = false;
state.statusReason = '';
state.isLoading = false; state.isLoading = false;
state.newIssues = data.newIssues; state.newIssues = data.newIssues;
state.resolvedIssues = data.resolvedIssues; state.resolvedIssues = data.resolvedIssues;
}, },
[types.RECEIVE_REPORTS_ERROR](state) { [types.RECEIVE_REPORTS_ERROR](state, error) {
state.isLoading = false; state.isLoading = false;
state.hasError = true; state.hasError = true;
state.statusReason = error?.response?.data?.status_reason;
}, },
}; };
export default () => ({ export default () => ({
basePath: null, basePath: null,
headPath: null, headPath: null,
reportsPath: null,
baseBlobPath: null, baseBlobPath: null,
headBlobPath: null, headBlobPath: null,
isLoading: false, isLoading: false,
hasError: false, hasError: false,
statusReason: '',
newIssues: [], newIssues: [],
resolvedIssues: [], resolvedIssues: [],
......
...@@ -3,8 +3,10 @@ import CodeQualityComparisonWorker from '../../workers/codequality_comparison_wo ...@@ -3,8 +3,10 @@ import CodeQualityComparisonWorker from '../../workers/codequality_comparison_wo
export const parseCodeclimateMetrics = (issues = [], path = '') => { export const parseCodeclimateMetrics = (issues = [], path = '') => {
return issues.map((issue) => { return issues.map((issue) => {
const parsedIssue = { const parsedIssue = {
...issue,
name: issue.description, name: issue.description,
path: issue.file_path,
urlPath: `${path}/${issue.file_path}#L${issue.line}`,
...issue,
}; };
if (issue?.location?.path) { if (issue?.location?.path) {
......
...@@ -464,6 +464,7 @@ export default { ...@@ -464,6 +464,7 @@ export default {
:head-path="mr.codeclimate.head_path" :head-path="mr.codeclimate.head_path"
:head-blob-path="mr.headBlobPath" :head-blob-path="mr.headBlobPath"
:base-blob-path="mr.baseBlobPath" :base-blob-path="mr.baseBlobPath"
:codequality-reports-path="mr.codequalityReportsPath"
:codequality-help-path="mr.codequalityHelpPath" :codequality-help-path="mr.codequalityHelpPath"
/> />
......
...@@ -241,10 +241,11 @@ export default class MergeRequestStore { ...@@ -241,10 +241,11 @@ export default class MergeRequestStore {
this.isDismissedSuggestPipeline = data.is_dismissed_suggest_pipeline; this.isDismissedSuggestPipeline = data.is_dismissed_suggest_pipeline;
this.securityReportsDocsPath = data.security_reports_docs_path; this.securityReportsDocsPath = data.security_reports_docs_path;
// codeclimate // code quality
const blobPath = data.blob_path || {}; const blobPath = data.blob_path || {};
this.headBlobPath = blobPath.head_path || ''; this.headBlobPath = blobPath.head_path || '';
this.baseBlobPath = blobPath.base_path || ''; this.baseBlobPath = blobPath.base_path || '';
this.codequalityReportsPath = data.codequality_reports_path;
this.codequalityHelpPath = data.codequality_help_path; this.codequalityHelpPath = data.codequality_help_path;
this.codeclimate = data.codeclimate; this.codeclimate = data.codeclimate;
......
...@@ -278,6 +278,7 @@ export default { ...@@ -278,6 +278,7 @@ export default {
:head-path="mr.codeclimate.head_path" :head-path="mr.codeclimate.head_path"
:head-blob-path="mr.headBlobPath" :head-blob-path="mr.headBlobPath"
:base-blob-path="mr.baseBlobPath" :base-blob-path="mr.baseBlobPath"
:codequality-reports-path="mr.codequalityReportsPath"
:codequality-help-path="mr.codequalityHelpPath" :codequality-help-path="mr.codequalityHelpPath"
/> />
<grouped-browser-performance-reports-app <grouped-browser-performance-reports-app
......
...@@ -88,3 +88,53 @@ export const issueDiff = [ ...@@ -88,3 +88,53 @@ export const issueDiff = [
urlPath: 'headPath/lib/six.rb#L6', urlPath: 'headPath/lib/six.rb#L6',
}, },
]; ];
export const reportIssues = {
status: 'failed',
new_errors: [
{
description:
'Method `long_if` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.',
severity: 'minor',
file_path: 'codequality.rb',
line: 5,
},
],
resolved_errors: [
{
description: 'Insecure Dependency',
severity: 'major',
file_path: 'lib/six.rb',
line: 22,
},
],
existing_errors: [],
summary: { total: 3, resolved: 0, errored: 3 },
};
export const parsedReportIssues = {
newIssues: [
{
description:
'Method `long_if` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.',
file_path: 'codequality.rb',
line: 5,
name:
'Method `long_if` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.',
path: 'codequality.rb',
severity: 'minor',
urlPath: 'null/codequality.rb#L5',
},
],
resolvedIssues: [
{
description: 'Insecure Dependency',
file_path: 'lib/six.rb',
line: 22,
name: 'Insecure Dependency',
path: 'lib/six.rb',
severity: 'major',
urlPath: 'null/lib/six.rb#L22',
},
],
};
...@@ -5,7 +5,14 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -5,7 +5,14 @@ import axios from '~/lib/utils/axios_utils';
import * as actions from '~/reports/codequality_report/store/actions'; import * as actions from '~/reports/codequality_report/store/actions';
import * as types from '~/reports/codequality_report/store/mutation_types'; import * as types from '~/reports/codequality_report/store/mutation_types';
import createStore from '~/reports/codequality_report/store'; import createStore from '~/reports/codequality_report/store';
import { headIssues, baseIssues, mockParsedHeadIssues, mockParsedBaseIssues } from '../mock_data'; import {
headIssues,
baseIssues,
mockParsedHeadIssues,
mockParsedBaseIssues,
reportIssues,
parsedReportIssues,
} from '../mock_data';
// mock codequality comparison worker // mock codequality comparison worker
jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () => jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () =>
...@@ -39,6 +46,7 @@ describe('Codequality Reports actions', () => { ...@@ -39,6 +46,7 @@ describe('Codequality Reports actions', () => {
headPath: 'headPath', headPath: 'headPath',
baseBlobPath: 'baseBlobPath', baseBlobPath: 'baseBlobPath',
headBlobPath: 'headBlobPath', headBlobPath: 'headBlobPath',
reportsPath: 'reportsPath',
helpPath: 'codequalityHelpPath', helpPath: 'codequalityHelpPath',
}; };
...@@ -55,68 +63,119 @@ describe('Codequality Reports actions', () => { ...@@ -55,68 +63,119 @@ describe('Codequality Reports actions', () => {
describe('fetchReports', () => { describe('fetchReports', () => {
let mock; let mock;
let diffFeatureFlagEnabled;
beforeEach(() => { describe('with codequalityMrDiff feature flag enabled', () => {
localState.headPath = `${TEST_HOST}/head.json`; beforeEach(() => {
localState.basePath = `${TEST_HOST}/base.json`; diffFeatureFlagEnabled = true;
mock = new MockAdapter(axios); localState.reportsPath = `${TEST_HOST}/codequality_reports.json`;
}); mock = new MockAdapter(axios);
});
afterEach(() => { afterEach(() => {
mock.restore(); mock.restore();
}); });
describe('on success', () => { describe('on success', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => { it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => {
mock.onGet(`${TEST_HOST}/head.json`).reply(200, headIssues); mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(200, reportIssues);
mock.onGet(`${TEST_HOST}/base.json`).reply(200, baseIssues);
testAction(
testAction( actions.fetchReports,
actions.fetchReports, diffFeatureFlagEnabled,
null, localState,
localState, [{ type: types.REQUEST_REPORTS }],
[{ type: types.REQUEST_REPORTS }], [
[ {
{ payload: parsedReportIssues,
payload: { type: 'receiveReportsSuccess',
newIssues: [mockParsedHeadIssues[0]],
resolvedIssues: [mockParsedBaseIssues[0]],
}, },
type: 'receiveReportsSuccess', ],
}, done,
], );
done, });
);
}); });
});
describe('on error', () => { describe('on error', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => { it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
mock.onGet(`${TEST_HOST}/head.json`).reply(500); mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(500);
testAction( testAction(
actions.fetchReports, actions.fetchReports,
null, diffFeatureFlagEnabled,
localState, localState,
[{ type: types.REQUEST_REPORTS }], [{ type: types.REQUEST_REPORTS }],
[{ type: 'receiveReportsError' }], [{ type: 'receiveReportsError', payload: expect.any(Error) }],
done, done,
); );
});
}); });
}); });
describe('with no base path', () => { describe('with codequalityMrDiff feature flag disabled', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => { beforeEach(() => {
localState.basePath = null; diffFeatureFlagEnabled = false;
localState.headPath = `${TEST_HOST}/head.json`;
testAction( localState.basePath = `${TEST_HOST}/base.json`;
actions.fetchReports, mock = new MockAdapter(axios);
null, });
localState,
[{ type: types.REQUEST_REPORTS }], afterEach(() => {
[{ type: 'receiveReportsError' }], mock.restore();
done, });
);
describe('on success', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => {
mock.onGet(`${TEST_HOST}/head.json`).reply(200, headIssues);
mock.onGet(`${TEST_HOST}/base.json`).reply(200, baseIssues);
testAction(
actions.fetchReports,
diffFeatureFlagEnabled,
localState,
[{ type: types.REQUEST_REPORTS }],
[
{
payload: {
newIssues: [mockParsedHeadIssues[0]],
resolvedIssues: [mockParsedBaseIssues[0]],
},
type: 'receiveReportsSuccess',
},
],
done,
);
});
});
describe('on error', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
mock.onGet(`${TEST_HOST}/head.json`).reply(500);
testAction(
actions.fetchReports,
diffFeatureFlagEnabled,
localState,
[{ type: types.REQUEST_REPORTS }],
[{ type: 'receiveReportsError', payload: expect.any(Error) }],
done,
);
});
});
describe('with no base path', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
localState.basePath = null;
testAction(
actions.fetchReports,
diffFeatureFlagEnabled,
localState,
[{ type: types.REQUEST_REPORTS }],
[{ type: 'receiveReportsError' }],
done,
);
});
}); });
}); });
}); });
...@@ -142,7 +201,7 @@ describe('Codequality Reports actions', () => { ...@@ -142,7 +201,7 @@ describe('Codequality Reports actions', () => {
actions.receiveReportsError, actions.receiveReportsError,
null, null,
localState, localState,
[{ type: types.RECEIVE_REPORTS_ERROR }], [{ type: types.RECEIVE_REPORTS_ERROR, payload: null }],
[], [],
done, done,
); );
......
...@@ -55,6 +55,12 @@ describe('Codequality Reports mutations', () => { ...@@ -55,6 +55,12 @@ describe('Codequality Reports mutations', () => {
expect(localState.hasError).toEqual(false); expect(localState.hasError).toEqual(false);
}); });
it('clears statusReason', () => {
mutations.RECEIVE_REPORTS_SUCCESS(localState, {});
expect(localState.statusReason).toEqual('');
});
it('sets newIssues and resolvedIssues from response data', () => { it('sets newIssues and resolvedIssues from response data', () => {
const data = { newIssues: [{ id: 1 }], resolvedIssues: [{ id: 2 }] }; const data = { newIssues: [{ id: 1 }], resolvedIssues: [{ id: 2 }] };
mutations.RECEIVE_REPORTS_SUCCESS(localState, data); mutations.RECEIVE_REPORTS_SUCCESS(localState, data);
...@@ -76,5 +82,13 @@ describe('Codequality Reports mutations', () => { ...@@ -76,5 +82,13 @@ describe('Codequality Reports mutations', () => {
expect(localState.hasError).toEqual(true); expect(localState.hasError).toEqual(true);
}); });
it('sets statusReason to string from error response data', () => {
const data = { status_reason: 'This merge request does not have codequality reports' };
const error = { response: { data } };
mutations.RECEIVE_REPORTS_ERROR(localState, error);
expect(localState.statusReason).toEqual(data.status_reason);
});
}); });
}); });
...@@ -2,7 +2,13 @@ import { ...@@ -2,7 +2,13 @@ import {
parseCodeclimateMetrics, parseCodeclimateMetrics,
doCodeClimateComparison, doCodeClimateComparison,
} from '~/reports/codequality_report/store/utils/codequality_comparison'; } from '~/reports/codequality_report/store/utils/codequality_comparison';
import { baseIssues, mockParsedHeadIssues, mockParsedBaseIssues } from '../../mock_data'; import {
baseIssues,
mockParsedHeadIssues,
mockParsedBaseIssues,
reportIssues,
parsedReportIssues,
} from '../../mock_data';
jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () => { jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () => {
let mockPostMessageCallback; let mockPostMessageCallback;
...@@ -34,7 +40,7 @@ describe('Codequality report store utils', () => { ...@@ -34,7 +40,7 @@ describe('Codequality report store utils', () => {
let result; let result;
describe('parseCodeclimateMetrics', () => { describe('parseCodeclimateMetrics', () => {
it('should parse the received issues', () => { it('should parse the issues from codeclimate artifacts', () => {
[result] = parseCodeclimateMetrics(baseIssues, 'path'); [result] = parseCodeclimateMetrics(baseIssues, 'path');
expect(result.name).toEqual(baseIssues[0].check_name); expect(result.name).toEqual(baseIssues[0].check_name);
...@@ -42,6 +48,14 @@ describe('Codequality report store utils', () => { ...@@ -42,6 +48,14 @@ describe('Codequality report store utils', () => {
expect(result.line).toEqual(baseIssues[0].location.lines.begin); expect(result.line).toEqual(baseIssues[0].location.lines.begin);
}); });
it('should parse the issues from backend codequality diff', () => {
[result] = parseCodeclimateMetrics(reportIssues.new_errors, 'path');
expect(result.name).toEqual(parsedReportIssues.newIssues[0].name);
expect(result.path).toEqual(parsedReportIssues.newIssues[0].path);
expect(result.line).toEqual(parsedReportIssues.newIssues[0].line);
});
describe('when an issue has no location or path', () => { describe('when an issue has no location or path', () => {
const issue = { description: 'Insecure Dependency' }; const issue = { description: 'Insecure Dependency' };
......
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