@@ -280,7 +280,7 @@ public class TelemetryData { | |||
record Project(String projectUuid, Long lastAnalysis, String language, Long loc) { | |||
} | |||
record QualityGate(String uuid, boolean isCaycCompliant) { | |||
record QualityGate(String uuid, String caycStatus) { | |||
} | |||
public static class ProjectStatistics { |
@@ -174,7 +174,7 @@ public class TelemetryDataJsonWriter { | |||
statistics.getQualityGates().forEach(qualityGate -> { | |||
json.beginObject(); | |||
json.prop("uuid", qualityGate.uuid()); | |||
json.prop("isCaycCompliant", qualityGate.isCaycCompliant()); | |||
json.prop("caycStatus", qualityGate.caycStatus()); | |||
json.endObject(); | |||
}); | |||
json.endArray(); |
@@ -453,15 +453,15 @@ public class TelemetryDataJsonWriterTest { | |||
"quality-gates": [ | |||
{ | |||
"uuid": "uuid-0", | |||
"isCaycCompliant": true | |||
"caycStatus": "non-compliant" | |||
}, | |||
{ | |||
"uuid": "uuid-1", | |||
"isCaycCompliant": false | |||
"caycStatus": "compliant" | |||
}, | |||
{ | |||
"uuid": "uuid-2", | |||
"isCaycCompliant": true | |||
"caycStatus": "over-compliant" | |||
} | |||
] | |||
} | |||
@@ -518,8 +518,9 @@ public class TelemetryDataJsonWriterTest { | |||
} | |||
private List<TelemetryData.QualityGate> attachQualityGates() { | |||
return IntStream.range(0, 3).mapToObj(i -> new TelemetryData.QualityGate("uuid-" + i, i % 2 == 0)) | |||
.collect(Collectors.toList()); | |||
return List.of(new TelemetryData.QualityGate("uuid-0", "non-compliant"), | |||
new TelemetryData.QualityGate("uuid-1", "compliant"), | |||
new TelemetryData.QualityGate("uuid-2", "over-compliant")); | |||
} | |||
@DataProvider |
@@ -23,7 +23,7 @@ import { mockQualityGate } from '../../helpers/mocks/quality-gates'; | |||
import { mockUserBase } from '../../helpers/mocks/users'; | |||
import { mockCondition, mockGroup } from '../../helpers/testMocks'; | |||
import { MetricKey } from '../../types/metrics'; | |||
import { Condition, QualityGate } from '../../types/types'; | |||
import { CaycStatus, Condition, QualityGate } from '../../types/types'; | |||
import { | |||
addGroup, | |||
addUser, | |||
@@ -84,7 +84,7 @@ export class QualityGatesServiceMock { | |||
], | |||
isDefault: true, | |||
isBuiltIn: false, | |||
isCaycCompliant: true, | |||
caycStatus: CaycStatus.Compliant, | |||
}), | |||
mockQualityGate({ | |||
name: 'SonarSource way - CFamily', | |||
@@ -95,6 +95,7 @@ export class QualityGatesServiceMock { | |||
], | |||
isDefault: false, | |||
isBuiltIn: false, | |||
caycStatus: CaycStatus.NonCompliant, | |||
}), | |||
mockQualityGate({ | |||
name: 'Sonar way', | |||
@@ -123,7 +124,7 @@ export class QualityGatesServiceMock { | |||
], | |||
isDefault: false, | |||
isBuiltIn: true, | |||
isCaycCompliant: true, | |||
caycStatus: CaycStatus.Compliant, | |||
}), | |||
mockQualityGate({ | |||
name: 'Non Cayc QG', | |||
@@ -134,14 +135,45 @@ export class QualityGatesServiceMock { | |||
], | |||
isDefault: false, | |||
isBuiltIn: false, | |||
isCaycCompliant: false, | |||
caycStatus: CaycStatus.NonCompliant, | |||
}), | |||
mockQualityGate({ | |||
name: 'Over Compliant CAYC QG', | |||
conditions: [ | |||
{ id: 'deprecatedoc', metric: 'function_complexity', op: 'LT', error: '1' }, | |||
{ id: 'AXJMbIUHPAOIsUIE3eOFoc', metric: 'new_coverage', op: 'LT', error: '80' }, | |||
{ id: 'AXJMbIUHPAOIsUIE3eNsoc', metric: 'new_security_rating', op: 'GT', error: '1' }, | |||
{ id: 'AXJMbIUHPAOIsUIE3eODoc', metric: 'new_reliability_rating', op: 'GT', error: '1' }, | |||
{ | |||
id: 'AXJMbIUHPAOIsUIE3eOEoc', | |||
metric: 'new_maintainability_rating', | |||
op: 'GT', | |||
error: '1', | |||
}, | |||
{ id: 'AXJMbIUHPAOIsUIE3eOFocdl', metric: 'new_coverage', op: 'LT', error: '80' }, | |||
{ | |||
id: 'AXJMbIUHPAOIsUIE3eOGoc', | |||
metric: 'new_duplicated_lines_density', | |||
op: 'GT', | |||
error: '3', | |||
}, | |||
{ | |||
id: 'AXJMbIUHPAOIsUIE3eOkoc', | |||
metric: 'new_security_hotspots_reviewed', | |||
op: 'LT', | |||
error: '100', | |||
}, | |||
], | |||
isDefault: false, | |||
isBuiltIn: false, | |||
caycStatus: CaycStatus.OverCompliant, | |||
}), | |||
mockQualityGate({ | |||
name: 'QG without conditions', | |||
conditions: [], | |||
isDefault: false, | |||
isBuiltIn: false, | |||
isCaycCompliant: false, | |||
caycStatus: CaycStatus.NonCompliant, | |||
}), | |||
mockQualityGate({ | |||
name: 'QG without new code conditions', | |||
@@ -150,7 +182,7 @@ export class QualityGatesServiceMock { | |||
], | |||
isDefault: false, | |||
isBuiltIn: false, | |||
isCaycCompliant: false, | |||
caycStatus: CaycStatus.NonCompliant, | |||
}), | |||
]; | |||
@@ -279,7 +311,7 @@ export class QualityGatesServiceMock { | |||
], | |||
isDefault: false, | |||
isBuiltIn: false, | |||
isCaycCompliant: true, | |||
caycStatus: CaycStatus.Compliant, | |||
}) | |||
); | |||
return this.reply({ |
@@ -0,0 +1,86 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import DocLink from '../../../components/common/DocLink'; | |||
import Link from '../../../components/common/Link'; | |||
import QualifierIcon from '../../../components/icons/QualifierIcon'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { getProjectQueryUrl } from '../../../helpers/urls'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { QualityGateStatus } from '../../../types/quality-gates'; | |||
import { CaycStatus } from '../../../types/types'; | |||
interface Props { | |||
projects: QualityGateStatus[]; | |||
caycStatus: CaycStatus; | |||
} | |||
export default function ApplicationNonCaycProjectWarning({ projects, caycStatus }: Props) { | |||
return ( | |||
<div className="overview-quality-gate-conditions-list padded big-spacer-top"> | |||
{caycStatus === CaycStatus.NonCompliant ? ( | |||
<Alert variant="warning"> | |||
{translateWithParameters( | |||
'overview.quality_gate.application.non_cayc.projects_x', | |||
projects.length | |||
)} | |||
</Alert> | |||
) : ( | |||
<p className="padded"> | |||
{translateWithParameters( | |||
'overview.quality_gate.application.cayc_over_compliant.projects_x', | |||
projects.length | |||
)} | |||
</p> | |||
)} | |||
<ul className="spacer-left spacer-bottom big-spacer-top"> | |||
{projects.map(({ key, name, branchLike }) => ( | |||
<li key={key} className="text-ellipsis spacer-bottom" title={name}> | |||
<Link | |||
className="link-no-underline" | |||
to={getProjectQueryUrl(key, getBranchLikeQuery(branchLike))} | |||
> | |||
<QualifierIcon | |||
className="little-spacer-right" | |||
qualifier={ComponentQualifier.Project} | |||
/> | |||
{name} | |||
</Link> | |||
</li> | |||
))} | |||
</ul> | |||
<hr className="big-spacer-top big-spacer-bottom" /> | |||
<div className="spacer spacer-bottom big-spacer-top"> | |||
{caycStatus === CaycStatus.NonCompliant ? ( | |||
<DocLink to="/user-guide/clean-as-you-code/"> | |||
{translate('overview.quality_gate.conditions.cayc.link')} | |||
</DocLink> | |||
) : ( | |||
<DocLink to="/user-guide/clean-as-you-code/#potential-drawbacks"> | |||
{translate('overview.quality_gate.conditions.cayc_over_compliant.link')} | |||
</DocLink> | |||
)} | |||
</div> | |||
</div> | |||
); | |||
} |
@@ -187,13 +187,13 @@ export default class BranchOverview extends React.PureComponent<Props, State> { | |||
if (this.mounted) { | |||
const qgStatuses = results | |||
.map(({ measures = [], project, projectBranchLike }) => { | |||
const { key, name, status, isCaycCompliant } = project; | |||
const { key, name, status, caycStatus } = project; | |||
const conditions = extractStatusConditionsFromApplicationStatusChildProject(project); | |||
const failedConditions = this.getFailedConditions(conditions, measures); | |||
return { | |||
failedConditions, | |||
isCaycCompliant, | |||
caycStatus, | |||
key, | |||
name, | |||
status, | |||
@@ -241,13 +241,13 @@ export default class BranchOverview extends React.PureComponent<Props, State> { | |||
this.loadMeasuresAndMeta(key, branch, metricKeys).then( | |||
({ measures, metrics, period }) => { | |||
if (this.mounted && measures) { | |||
const { ignoredConditions, isCaycCompliant, status } = projectStatus; | |||
const { ignoredConditions, caycStatus, status } = projectStatus; | |||
const conditions = extractStatusConditionsFromProjectStatus(projectStatus); | |||
const failedConditions = this.getFailedConditions(conditions, measures); | |||
const qgStatus = { | |||
ignoredConditions, | |||
isCaycCompliant, | |||
caycStatus, | |||
failedConditions, | |||
key, | |||
name, |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import DocLink from '../../../components/common/DocLink'; | |||
import Link from '../../../components/common/Link'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
@@ -33,17 +34,24 @@ export default function CleanAsYouCodeWarning({ component }: Props) { | |||
return ( | |||
<> | |||
<Alert variant="warning">{translate('overview.quality_gate.conditions.cayc.warning')}</Alert> | |||
<p className="big-spacer-top big-spacer-bottom"> | |||
{translate('overview.quality_gate.conditions.cayc.details')} | |||
</p> | |||
{component.qualityGate && ( | |||
<div className="big-spacer-bottom"> | |||
<Link className="button" to={getQualityGateUrl(component.qualityGate.name)}> | |||
{translate('overview.quality_gate.conditions.cayc.review')} | |||
</Link> | |||
</div> | |||
{component.qualityGate ? ( | |||
<p className="big-spacer-top big-spacer-bottom"> | |||
<FormattedMessage | |||
id="overview.quality_gate.conditions.cayc.details_with_link" | |||
defaultMessage={translate('overview.quality_gate.conditions.cayc.details_with_link')} | |||
values={{ | |||
link: ( | |||
<Link to={getQualityGateUrl(component.qualityGate.name)}> | |||
{translate('overview.quality_gate.conditions.non_cayc.warning.link')} | |||
</Link> | |||
), | |||
}} | |||
/> | |||
</p> | |||
) : ( | |||
<p className="big-spacer-top big-spacer-bottom"> | |||
{translate('overview.quality_gate.conditions.cayc.details')} | |||
</p> | |||
)} | |||
<DocLink to="/user-guide/clean-as-you-code/"> |
@@ -0,0 +1,62 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import DocLink from '../../../components/common/DocLink'; | |||
import Link from '../../../components/common/Link'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getQualityGateUrl } from '../../../helpers/urls'; | |||
import { Component } from '../../../types/types'; | |||
interface Props { | |||
component: Pick<Component, 'key' | 'qualifier' | 'qualityGate'>; | |||
} | |||
export default function CleanAsYouCodeWarningOverCompliant({ component }: Props) { | |||
return ( | |||
<> | |||
{component.qualityGate ? ( | |||
<p className="big-spacer-bottom"> | |||
<FormattedMessage | |||
id="overview.quality_gate.conditions.cayc_over_compliant.details_with_link" | |||
defaultMessage={translate( | |||
'overview.quality_gate.conditions.cayc_over_compliant.details_with_link' | |||
)} | |||
values={{ | |||
link: ( | |||
<Link to={getQualityGateUrl(component.qualityGate.name)}> | |||
{translate('overview.quality_gate.conditions.cayc_over_compliant.warning.link')} | |||
</Link> | |||
), | |||
}} | |||
/> | |||
</p> | |||
) : ( | |||
<p className="big-spacer-bottom"> | |||
{translate('overview.quality_gate.conditions.cayc_over_compliant.details')} | |||
</p> | |||
)} | |||
<DocLink to="/user-guide/clean-as-you-code/#potential-drawbacks"> | |||
{translate('overview.quality_gate.conditions.cayc_over_compliant.link')} | |||
</DocLink> | |||
</> | |||
); | |||
} |
@@ -20,18 +20,15 @@ | |||
import classNames from 'classnames'; | |||
import { flatMap } from 'lodash'; | |||
import * as React from 'react'; | |||
import Link from '../../../components/common/Link'; | |||
import HelpTooltip from '../../../components/controls/HelpTooltip'; | |||
import QualifierIcon from '../../../components/icons/QualifierIcon'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { getProjectQueryUrl } from '../../../helpers/urls'; | |||
import { ComponentQualifier, isApplication } from '../../../types/component'; | |||
import { QualityGateStatus } from '../../../types/quality-gates'; | |||
import { Component } from '../../../types/types'; | |||
import { CaycStatus, Component } from '../../../types/types'; | |||
import SonarLintPromotion from '../components/SonarLintPromotion'; | |||
import ApplicationNonCaycProjectWarning from './ApplicationNonCaycProjectWarning'; | |||
import QualityGatePanelSection from './QualityGatePanelSection'; | |||
export interface QualityGatePanelProps { | |||
@@ -57,7 +54,13 @@ export function QualityGatePanel(props: QualityGatePanelProps) { | |||
const nonCaycProjectsInApp = isApplication(component.qualifier) | |||
? qgStatuses | |||
.filter(({ isCaycCompliant }) => !isCaycCompliant) | |||
.filter(({ caycStatus }) => caycStatus === CaycStatus.NonCompliant) | |||
.sort(({ name: a }, { name: b }) => a.localeCompare(b, undefined, { sensitivity: 'base' })) | |||
: []; | |||
const overCompliantCaycProjectsInApp = isApplication(component.qualifier) | |||
? qgStatuses | |||
.filter(({ caycStatus }) => caycStatus === CaycStatus.OverCompliant) | |||
.sort(({ name: a }, { name: b }) => a.localeCompare(b, undefined, { sensitivity: 'base' })) | |||
: []; | |||
@@ -118,7 +121,7 @@ export function QualityGatePanel(props: QualityGatePanelProps) { | |||
</div> | |||
{(overallFailedConditionsCount > 0 || | |||
qgStatuses.some(({ isCaycCompliant }) => !isCaycCompliant)) && ( | |||
qgStatuses.some(({ caycStatus }) => caycStatus !== CaycStatus.Compliant)) && ( | |||
<div data-test="overview__quality-gate-conditions"> | |||
{qgStatuses.map((qgStatus) => ( | |||
<QualityGatePanelSection | |||
@@ -131,39 +134,17 @@ export function QualityGatePanel(props: QualityGatePanelProps) { | |||
)} | |||
{nonCaycProjectsInApp.length > 0 && ( | |||
<div className="overview-quality-gate-conditions-list padded big-spacer-top"> | |||
<Alert variant="warning"> | |||
{translateWithParameters( | |||
'overview.quality_gate.application.non_cayc.projects_x', | |||
nonCaycProjectsInApp.length | |||
)} | |||
</Alert> | |||
<div className="spacer big-spacer-bottom big-spacer-top"> | |||
<Link | |||
target="_blank" | |||
to="https://docs.sonarqube.org/latest/user-guide/clean-as-you-code/#quality-gate" | |||
> | |||
{translate('overview.quality_gate.conditions.cayc.link')} | |||
</Link> | |||
</div> | |||
<hr className="big-spacer-top big-spacer-bottom" /> | |||
<ul className="spacer-left spacer-bottom"> | |||
{nonCaycProjectsInApp.map(({ key, name, branchLike }) => ( | |||
<li key={key} className="text-ellipsis spacer-bottom" title={name}> | |||
<Link | |||
className="link-no-underline" | |||
to={getProjectQueryUrl(key, getBranchLikeQuery(branchLike))} | |||
> | |||
<QualifierIcon | |||
className="little-spacer-right" | |||
qualifier={ComponentQualifier.Project} | |||
/> | |||
{name} | |||
</Link> | |||
</li> | |||
))} | |||
</ul> | |||
</div> | |||
<ApplicationNonCaycProjectWarning | |||
projects={nonCaycProjectsInApp} | |||
caycStatus={CaycStatus.NonCompliant} | |||
/> | |||
)} | |||
{overCompliantCaycProjectsInApp.length > 0 && ( | |||
<ApplicationNonCaycProjectWarning | |||
projects={overCompliantCaycProjectsInApp} | |||
caycStatus={CaycStatus.OverCompliant} | |||
/> | |||
)} | |||
</> | |||
)} |
@@ -29,9 +29,10 @@ import { | |||
QualityGateStatus, | |||
QualityGateStatusConditionEnhanced, | |||
} from '../../../types/quality-gates'; | |||
import { Component } from '../../../types/types'; | |||
import { CaycStatus, Component } from '../../../types/types'; | |||
import QualityGateConditions from '../components/QualityGateConditions'; | |||
import CleanAsYouCodeWarning from './CleanAsYouCodeWarning'; | |||
import CleanAsYouCodeWarningOverCompliant from './CleanAsYouCodeWarningOverCompliant'; | |||
export interface QualityGatePanelSectionProps { | |||
branchLike?: BranchLike; | |||
@@ -85,7 +86,7 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) { | |||
if ( | |||
!( | |||
qgStatus.failedConditions.length > 0 || | |||
(!qgStatus.isCaycCompliant && !isApplication(component.qualifier)) | |||
(qgStatus.caycStatus !== CaycStatus.Compliant && !isApplication(component.qualifier)) | |||
) | |||
) { | |||
return null; | |||
@@ -99,7 +100,7 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) { | |||
const showSectionTitles = | |||
isApplication(component.qualifier) || | |||
!qgStatus.isCaycCompliant || | |||
qgStatus.caycStatus !== CaycStatus.Compliant || | |||
(overallFailedConditions.length > 0 && newCodeFailedConditions.length > 0); | |||
const toggleLabel = collapsed | |||
@@ -130,11 +131,19 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) { | |||
{!collapsed && ( | |||
<> | |||
{!qgStatus.isCaycCompliant && !isApplication(component.qualifier) && ( | |||
<div className="big-padded bordered-bottom overview-quality-gate-conditions-list"> | |||
<CleanAsYouCodeWarning component={component} /> | |||
</div> | |||
)} | |||
{qgStatus.caycStatus === CaycStatus.NonCompliant && | |||
!isApplication(component.qualifier) && ( | |||
<div className="big-padded bordered-bottom overview-quality-gate-conditions-list"> | |||
<CleanAsYouCodeWarning component={component} /> | |||
</div> | |||
)} | |||
{qgStatus.caycStatus === CaycStatus.OverCompliant && | |||
!isApplication(component.qualifier) && ( | |||
<div className="big-padded bordered-bottom overview-quality-gate-conditions-list"> | |||
<CleanAsYouCodeWarningOverCompliant component={component} /> | |||
</div> | |||
)} | |||
{newCodeFailedConditions.length > 0 && ( | |||
<> |
@@ -42,7 +42,7 @@ import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { MetricKey } from '../../../../types/metrics'; | |||
import { GraphType } from '../../../../types/project-activity'; | |||
import { Measure, Metric } from '../../../../types/types'; | |||
import { CaycStatus, Measure, Metric } from '../../../../types/types'; | |||
import BranchOverview, { BRANCH_OVERVIEW_ACTIVITY_GRAPH, NO_CI_DETECTED } from '../BranchOverview'; | |||
jest.mock('../../../../api/measures', () => { | |||
@@ -223,7 +223,7 @@ describe('project overview', () => { | |||
jest | |||
.mocked(getQualityGateProjectStatus) | |||
.mockResolvedValueOnce( | |||
mockQualityGateProjectStatus({ status: 'OK', isCaycCompliant: false }) | |||
mockQualityGateProjectStatus({ status: 'OK', caycStatus: CaycStatus.NonCompliant }) | |||
); | |||
renderBranchOverview(); | |||
@@ -267,9 +267,27 @@ describe('application overview', () => { | |||
it("should show projects that don't have a compliant quality gate", async () => { | |||
const appStatus = mockQualityGateApplicationStatus({ | |||
projects: [ | |||
{ key: '1', name: 'first project', conditions: [], isCaycCompliant: false, status: 'OK' }, | |||
{ key: '2', name: 'second', conditions: [], isCaycCompliant: true, status: 'OK' }, | |||
{ key: '3', name: 'number 3', conditions: [], isCaycCompliant: false, status: 'OK' }, | |||
{ | |||
key: '1', | |||
name: 'first project', | |||
conditions: [], | |||
caycStatus: CaycStatus.NonCompliant, | |||
status: 'OK', | |||
}, | |||
{ | |||
key: '2', | |||
name: 'second', | |||
conditions: [], | |||
caycStatus: CaycStatus.Compliant, | |||
status: 'OK', | |||
}, | |||
{ | |||
key: '3', | |||
name: 'number 3', | |||
conditions: [], | |||
caycStatus: CaycStatus.NonCompliant, | |||
status: 'OK', | |||
}, | |||
{ | |||
key: '4', | |||
name: 'four', | |||
@@ -282,7 +300,7 @@ describe('application overview', () => { | |||
errorThreshold: '0', | |||
}, | |||
], | |||
isCaycCompliant: false, | |||
caycStatus: CaycStatus.NonCompliant, | |||
status: 'ERROR', | |||
}, | |||
], |
@@ -27,6 +27,7 @@ import { | |||
} from '../../../../helpers/mocks/quality-gates'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { MetricKey } from '../../../../types/metrics'; | |||
import { CaycStatus } from '../../../../types/types'; | |||
import { QualityGatePanelSection, QualityGatePanelSectionProps } from '../QualityGatePanelSection'; | |||
it('should render correctly', () => { | |||
@@ -36,6 +37,7 @@ it('should render correctly', () => { | |||
qgStatus: mockQualityGateStatus({ | |||
failedConditions: [], | |||
status: 'OK', | |||
caycStatus: CaycStatus.Compliant, | |||
}), | |||
}).type() | |||
).toBeNull(); | |||
@@ -55,7 +57,7 @@ function shallowRender(props: Partial<QualityGatePanelSectionProps> = {}) { | |||
mockQualityGateStatusConditionEnhanced({ metric: MetricKey.new_bugs }), | |||
], | |||
status: 'ERROR', | |||
isCaycCompliant: false, | |||
caycStatus: CaycStatus.NonCompliant, | |||
})} | |||
{...props} | |||
/> |
@@ -69,6 +69,7 @@ exports[`should render correctly for applications 1`] = ` | |||
key="foo" | |||
qgStatus={ | |||
{ | |||
"caycStatus": "compliant", | |||
"failedConditions": [ | |||
{ | |||
"actual": "10", | |||
@@ -95,7 +96,6 @@ exports[`should render correctly for applications 1`] = ` | |||
}, | |||
], | |||
"ignoredConditions": false, | |||
"isCaycCompliant": true, | |||
"key": "foo", | |||
"name": "Foo", | |||
"status": "ERROR", | |||
@@ -128,6 +128,7 @@ exports[`should render correctly for applications 1`] = ` | |||
key="foo" | |||
qgStatus={ | |||
{ | |||
"caycStatus": "compliant", | |||
"failedConditions": [ | |||
{ | |||
"actual": "10", | |||
@@ -177,7 +178,6 @@ exports[`should render correctly for applications 1`] = ` | |||
}, | |||
], | |||
"ignoredConditions": false, | |||
"isCaycCompliant": true, | |||
"key": "foo", | |||
"name": "Foo", | |||
"status": "ERROR", | |||
@@ -333,6 +333,7 @@ exports[`should render correctly for applications 2`] = ` | |||
key="foo" | |||
qgStatus={ | |||
{ | |||
"caycStatus": "compliant", | |||
"failedConditions": [ | |||
{ | |||
"actual": "10", | |||
@@ -359,7 +360,6 @@ exports[`should render correctly for applications 2`] = ` | |||
}, | |||
], | |||
"ignoredConditions": false, | |||
"isCaycCompliant": true, | |||
"key": "foo", | |||
"name": "Foo", | |||
"status": "ERROR", | |||
@@ -392,9 +392,9 @@ exports[`should render correctly for applications 2`] = ` | |||
key="foo" | |||
qgStatus={ | |||
{ | |||
"caycStatus": "compliant", | |||
"failedConditions": [], | |||
"ignoredConditions": false, | |||
"isCaycCompliant": true, | |||
"key": "foo", | |||
"name": "Foo", | |||
"status": "OK", | |||
@@ -504,6 +504,7 @@ exports[`should render correctly for projects 1`] = ` | |||
key="foo" | |||
qgStatus={ | |||
{ | |||
"caycStatus": "compliant", | |||
"failedConditions": [ | |||
{ | |||
"actual": "10", | |||
@@ -530,7 +531,6 @@ exports[`should render correctly for projects 1`] = ` | |||
}, | |||
], | |||
"ignoredConditions": false, | |||
"isCaycCompliant": true, | |||
"key": "foo", | |||
"name": "Foo", | |||
"status": "ERROR", | |||
@@ -702,6 +702,7 @@ exports[`should render correctly for projects 3`] = ` | |||
key="foo" | |||
qgStatus={ | |||
{ | |||
"caycStatus": "compliant", | |||
"failedConditions": [ | |||
{ | |||
"actual": "10", | |||
@@ -728,7 +729,6 @@ exports[`should render correctly for projects 3`] = ` | |||
}, | |||
], | |||
"ignoredConditions": true, | |||
"isCaycCompliant": true, | |||
"key": "foo", | |||
"name": "Foo", | |||
"status": "ERROR", |
@@ -40,6 +40,7 @@ exports[`should render correctly 1`] = ` | |||
<Memo(QualityGateConditions) | |||
component={ | |||
{ | |||
"caycStatus": "non-compliant", | |||
"failedConditions": [ | |||
{ | |||
"actual": "10", | |||
@@ -89,7 +90,6 @@ exports[`should render correctly 1`] = ` | |||
}, | |||
], | |||
"ignoredConditions": false, | |||
"isCaycCompliant": false, | |||
"key": "foo", | |||
"name": "Foo", | |||
"status": "ERROR", | |||
@@ -131,6 +131,7 @@ exports[`should render correctly 1`] = ` | |||
<Memo(QualityGateConditions) | |||
component={ | |||
{ | |||
"caycStatus": "non-compliant", | |||
"failedConditions": [ | |||
{ | |||
"actual": "10", | |||
@@ -180,7 +181,6 @@ exports[`should render correctly 1`] = ` | |||
}, | |||
], | |||
"ignoredConditions": false, | |||
"isCaycCompliant": false, | |||
"key": "foo", | |||
"name": "Foo", | |||
"status": "ERROR", | |||
@@ -251,6 +251,7 @@ exports[`should render correctly 2`] = ` | |||
<Memo(QualityGateConditions) | |||
component={ | |||
{ | |||
"caycStatus": "non-compliant", | |||
"failedConditions": [ | |||
{ | |||
"actual": "10", | |||
@@ -300,7 +301,6 @@ exports[`should render correctly 2`] = ` | |||
}, | |||
], | |||
"ignoredConditions": false, | |||
"isCaycCompliant": false, | |||
"key": "foo", | |||
"name": "Foo", | |||
"status": "ERROR", | |||
@@ -342,6 +342,7 @@ exports[`should render correctly 2`] = ` | |||
<Memo(QualityGateConditions) | |||
component={ | |||
{ | |||
"caycStatus": "non-compliant", | |||
"failedConditions": [ | |||
{ | |||
"actual": "10", | |||
@@ -391,7 +392,6 @@ exports[`should render correctly 2`] = ` | |||
}, | |||
], | |||
"ignoredConditions": false, | |||
"isCaycCompliant": false, | |||
"key": "foo", | |||
"name": "Foo", | |||
"status": "ERROR", |
@@ -0,0 +1,35 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import DocLink from '../../../components/common/DocLink'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default function CaycOverCompliantBadgeTooltip() { | |||
return ( | |||
<div> | |||
<p className="spacer-bottom padded-bottom bordered-bottom-cayc"> | |||
{translate('quality_gates.cayc_over_compliant.tooltip.message')} | |||
</p> | |||
<DocLink to="/user-guide/clean-as-you-code/#potential-drawbacks"> | |||
{translate('quality_gates.cayc_over_compliant.badge.tooltip.learn_more')} | |||
</DocLink> | |||
</div> | |||
); | |||
} |
@@ -21,11 +21,25 @@ import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { deleteCondition } from '../../../api/quality-gates'; | |||
import withMetricsContext from '../../../app/components/metrics/withMetricsContext'; | |||
import { colors } from '../../../app/theme'; | |||
import { DeleteButton, EditButton } from '../../../components/controls/buttons'; | |||
import ConfirmModal from '../../../components/controls/ConfirmModal'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import InfoIcon from '../../../components/icons/InfoIcon'; | |||
import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types'; | |||
import { CAYC_CONDITIONS_WITHOUT_FIXED_VALUE, getLocalizedMetricNameNoDiffMetric } from '../utils'; | |||
import { | |||
CaycStatus, | |||
Condition as ConditionType, | |||
Dict, | |||
Metric, | |||
QualityGate, | |||
} from '../../../types/types'; | |||
import { | |||
CAYC_CONDITIONS_WITH_FIXED_VALUE, | |||
getLocalizedMetricNameNoDiffMetric, | |||
isCaycCondition, | |||
} from '../utils'; | |||
import CaycOverCompliantBadgeTooltip from './CaycOverCompliantBadgeTooltip'; | |||
import ConditionModal from './ConditionModal'; | |||
import ConditionValue from './ConditionValue'; | |||
@@ -47,6 +61,7 @@ interface State { | |||
modal: boolean; | |||
} | |||
const TOOLTIP_MOUSE_LEAVE_DELAY = 0.3; | |||
export class ConditionComponent extends React.PureComponent<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
@@ -103,6 +118,8 @@ export class ConditionComponent extends React.PureComponent<Props, State> { | |||
isCaycModal = false, | |||
} = this.props; | |||
const isCaycCompliantAndOverCompliant = qualityGate.caycStatus !== CaycStatus.NonCompliant; | |||
return ( | |||
<tr className={classNames({ highlighted: updated })}> | |||
<td className="text-middle"> | |||
@@ -119,15 +136,25 @@ export class ConditionComponent extends React.PureComponent<Props, State> { | |||
metric={metric} | |||
isCaycModal={isCaycModal} | |||
condition={condition} | |||
isCaycCompliant={qualityGate.isCaycCompliant} | |||
isCaycCompliantAndOverCompliant={isCaycCompliantAndOverCompliant} | |||
/> | |||
</td> | |||
<td className="text-middle nowrap display-flex-justify-end"> | |||
{!isCaycCondition(condition) && isCaycCompliantAndOverCompliant && ( | |||
<span className="display-flex-center spacer-right"> | |||
<Tooltip | |||
overlay={<CaycOverCompliantBadgeTooltip />} | |||
mouseLeaveDelay={TOOLTIP_MOUSE_LEAVE_DELAY} | |||
> | |||
<InfoIcon fill={colors.alertIconInfo} /> | |||
</Tooltip> | |||
</span> | |||
)} | |||
{!isCaycModal && canEdit && ( | |||
<> | |||
{(!qualityGate.isCaycCompliant || | |||
CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(condition.metric) || | |||
(qualityGate.isCaycCompliant && showEdit)) && ( | |||
{(!isCaycCompliantAndOverCompliant || | |||
!CAYC_CONDITIONS_WITH_FIXED_VALUE.includes(condition.metric) || | |||
(isCaycCompliantAndOverCompliant && showEdit)) && ( | |||
<> | |||
<EditButton | |||
aria-label={translateWithParameters( | |||
@@ -150,7 +177,9 @@ export class ConditionComponent extends React.PureComponent<Props, State> { | |||
)} | |||
</> | |||
)} | |||
{(!qualityGate.isCaycCompliant || (qualityGate.isCaycCompliant && showEdit)) && ( | |||
{(!isCaycCompliantAndOverCompliant || | |||
!isCaycCondition(condition) || | |||
(isCaycCompliantAndOverCompliant && showEdit)) && ( | |||
<> | |||
<DeleteButton | |||
aria-label={translateWithParameters( |
@@ -20,16 +20,12 @@ | |||
import { sortBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { createCondition, deleteCondition, updateCondition } from '../../../api/quality-gates'; | |||
import { createCondition, updateCondition } from '../../../api/quality-gates'; | |||
import DocLink from '../../../components/common/DocLink'; | |||
import ConfirmModal from '../../../components/controls/ConfirmModal'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { Condition, Dict, Metric, QualityGate } from '../../../types/types'; | |||
import { | |||
getCaycConditionsWithCorrectValue, | |||
getCorrectCaycCondition, | |||
getWeakMissingAndNonCaycConditions, | |||
} from '../utils'; | |||
import { getCorrectCaycCondition, getWeakMissingAndNonCaycConditions } from '../utils'; | |||
import ConditionsTable from './ConditionsTable'; | |||
interface Props { | |||
@@ -50,8 +46,7 @@ export default class CaycReviewUpdateConditionsModal extends React.PureComponent | |||
updateCaycQualityGate = () => { | |||
const { conditions, qualityGate } = this.props; | |||
const promiseArr: Promise<Condition | undefined | void>[] = []; | |||
const { weakConditions, missingConditions, nonCaycConditions } = | |||
getWeakMissingAndNonCaycConditions(conditions); | |||
const { weakConditions, missingConditions } = getWeakMissingAndNonCaycConditions(conditions); | |||
weakConditions.forEach((condition) => { | |||
promiseArr.push( | |||
@@ -80,14 +75,6 @@ export default class CaycReviewUpdateConditionsModal extends React.PureComponent | |||
); | |||
}); | |||
nonCaycConditions.forEach((condition) => { | |||
promiseArr.push( | |||
deleteCondition({ id: condition.id }) | |||
.then(() => this.props.onRemoveCondition(condition)) | |||
.catch(() => undefined) | |||
); | |||
}); | |||
return Promise.all(promiseArr).then(() => { | |||
this.props.lockEditing(); | |||
}); | |||
@@ -95,11 +82,18 @@ export default class CaycReviewUpdateConditionsModal extends React.PureComponent | |||
render() { | |||
const { conditions, qualityGate, metrics } = this.props; | |||
const caycConditionsWithCorrectValue = getCaycConditionsWithCorrectValue(conditions); | |||
const sortedConditions = sortBy( | |||
caycConditionsWithCorrectValue, | |||
const { weakConditions, missingConditions } = getWeakMissingAndNonCaycConditions(conditions); | |||
const sortedWeakConditions = sortBy( | |||
weakConditions, | |||
(condition) => metrics[condition.metric] && metrics[condition.metric].name | |||
); | |||
const sortedMissingConditions = sortBy( | |||
missingConditions, | |||
(condition) => metrics[condition.metric] && metrics[condition.metric].name | |||
); | |||
return ( | |||
<ConfirmModal | |||
header={translateWithParameters( | |||
@@ -125,18 +119,44 @@ export default class CaycReviewUpdateConditionsModal extends React.PureComponent | |||
}} | |||
/> | |||
</p> | |||
<p className="big-spacer-top big-spacer-bottom"> | |||
{sortedMissingConditions.length > 0 && ( | |||
<> | |||
<h4 className="big-spacer-top spacer-bottom"> | |||
{translateWithParameters( | |||
'quality_gates.cayc.review_update_modal.add_condition.header', | |||
sortedMissingConditions.length | |||
)} | |||
</h4> | |||
<ConditionsTable | |||
{...this.props} | |||
conditions={sortedMissingConditions} | |||
showEdit={false} | |||
isCaycModal={true} | |||
/> | |||
</> | |||
)} | |||
{sortedWeakConditions.length > 0 && ( | |||
<> | |||
<h4 className="big-spacer-top spacer-bottom"> | |||
{translateWithParameters( | |||
'quality_gates.cayc.review_update_modal.modify_condition.header', | |||
sortedWeakConditions.length | |||
)} | |||
</h4> | |||
<ConditionsTable | |||
{...this.props} | |||
conditions={sortedWeakConditions} | |||
showEdit={false} | |||
isCaycModal={true} | |||
/> | |||
</> | |||
)} | |||
<h4 className="big-spacer-top spacer-bottom"> | |||
{translate('quality_gates.cayc.review_update_modal.description2')} | |||
</p> | |||
<h3 className="medium text-normal spacer-top spacer-bottom"> | |||
{translate('quality_gates.conditions.new_code', 'long')} | |||
</h3> | |||
<ConditionsTable | |||
{...this.props} | |||
conditions={sortedConditions} | |||
showEdit={false} | |||
isCaycModal={true} | |||
/> | |||
</h4> | |||
</div> | |||
</ConfirmModal> | |||
); |
@@ -17,6 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import { Condition, Metric } from '../../../types/types'; | |||
@@ -27,17 +28,33 @@ interface Props { | |||
condition: Condition; | |||
isCaycModal?: boolean; | |||
metric: Metric; | |||
isCaycCompliant?: boolean; | |||
isCaycCompliantAndOverCompliant?: boolean; | |||
} | |||
function ConditionValue({ condition, isCaycModal, metric, isCaycCompliant }: Props) { | |||
function ConditionValue({ | |||
condition, | |||
isCaycModal, | |||
metric, | |||
isCaycCompliantAndOverCompliant, | |||
}: Props) { | |||
if (isCaycModal) { | |||
const isToBeModified = condition.error !== getCorrectCaycCondition(condition).error; | |||
return ( | |||
<> | |||
<span className="spacer-right"> | |||
{isToBeModified && ( | |||
<span className="red-text strike-through spacer-right"> | |||
{formatMeasure(condition.error, metric.type)} | |||
</span> | |||
)} | |||
<span className={classNames('spacer-right', { 'green-text': isToBeModified })}> | |||
{formatMeasure(getCorrectCaycCondition(condition).error, metric.type)} | |||
</span> | |||
<ConditionValueDescription condition={getCorrectCaycCondition(condition)} metric={metric} /> | |||
<ConditionValueDescription | |||
className={classNames({ 'green-text': isToBeModified })} | |||
condition={getCorrectCaycCondition(condition)} | |||
metric={metric} | |||
/> | |||
</> | |||
); | |||
} | |||
@@ -45,7 +62,7 @@ function ConditionValue({ condition, isCaycModal, metric, isCaycCompliant }: Pro | |||
return ( | |||
<> | |||
<span className="spacer-right">{formatMeasure(condition.error, metric.type)}</span> | |||
{isCaycCompliant && isCaycCondition(condition) && ( | |||
{isCaycCompliantAndOverCompliant && isCaycCondition(condition) && ( | |||
<ConditionValueDescription condition={condition} metric={metric} /> | |||
)} | |||
</> |
@@ -33,7 +33,13 @@ import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; | |||
import { isDiffMetric } from '../../../helpers/measures'; | |||
import { Feature } from '../../../types/features'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types'; | |||
import { | |||
CaycStatus, | |||
Condition as ConditionType, | |||
Dict, | |||
Metric, | |||
QualityGate, | |||
} from '../../../types/types'; | |||
import ConditionModal from './ConditionModal'; | |||
import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal'; | |||
import ConditionsTable from './ConditionsTable'; | |||
@@ -63,14 +69,14 @@ export class Conditions extends React.PureComponent<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
unlockEditing: !props.qualityGate.isCaycCompliant, | |||
unlockEditing: props.qualityGate.caycStatus === CaycStatus.NonCompliant, | |||
}; | |||
} | |||
componentDidUpdate(prevProps: Readonly<Props>): void { | |||
const { qualityGate } = this.props; | |||
if (prevProps.qualityGate.name !== qualityGate.name) { | |||
this.setState({ unlockEditing: !qualityGate.isCaycCompliant }); | |||
this.setState({ unlockEditing: qualityGate.caycStatus === CaycStatus.NonCompliant }); | |||
} | |||
} | |||
@@ -164,7 +170,7 @@ export class Conditions extends React.PureComponent<Props, State> { | |||
return ( | |||
<div className="quality-gate-section"> | |||
{qualityGate.isCaycCompliant && ( | |||
{qualityGate.caycStatus !== CaycStatus.NonCompliant && ( | |||
<Alert className="big-spacer-top big-spacer-bottom cayc-success-banner" variant="success"> | |||
<h4 className="spacer-bottom cayc-success-header"> | |||
{translate('quality_gates.cayc.banner.title')} | |||
@@ -180,7 +186,7 @@ export class Conditions extends React.PureComponent<Props, State> { | |||
</DocLink> | |||
), | |||
new_code_link: ( | |||
<DocLink to="/project-administration/defining-new-code//"> | |||
<DocLink to="/project-administration/defining-new-code/"> | |||
{translate('quality_gates.cayc.new_code')} | |||
</DocLink> | |||
), | |||
@@ -197,7 +203,29 @@ export class Conditions extends React.PureComponent<Props, State> { | |||
</Alert> | |||
)} | |||
{!qualityGate.isCaycCompliant && ( | |||
{qualityGate.caycStatus === CaycStatus.OverCompliant && ( | |||
<Alert className="big-spacer-top big-spacer-bottom cayc-success-banner" variant="info"> | |||
<h4 className="spacer-bottom cayc-over-compliant-header"> | |||
{translate('quality_gates.cayc_over_compliant.banner.title')} | |||
</h4> | |||
<p>{translate('quality_gates.cayc_over_compliant.banner.description1')}</p> | |||
<div className="cayc-warning-description spacer-top"> | |||
<FormattedMessage | |||
id="quality_gates.cayc_over_compliant.banner.description2" | |||
defaultMessage={translate('quality_gates.cayc_over_compliant.banner.description2')} | |||
values={{ | |||
link: ( | |||
<DocLink to="/user-guide/clean-as-you-code/#potential-drawbacks"> | |||
{translate('quality_gates.cayc_over_compliant.banner.link')} | |||
</DocLink> | |||
), | |||
}} | |||
/> | |||
</div> | |||
</Alert> | |||
)} | |||
{qualityGate.caycStatus === CaycStatus.NonCompliant && ( | |||
<Alert className="big-spacer-top big-spacer-bottom" variant="warning"> | |||
<h4 className="spacer-bottom cayc-warning-header"> | |||
{translate('quality_gates.cayc_missing.banner.title')} | |||
@@ -227,18 +255,17 @@ export class Conditions extends React.PureComponent<Props, State> { | |||
</Alert> | |||
)} | |||
{(!qualityGate.isCaycCompliant || (qualityGate.isCaycCompliant && unlockEditing)) && | |||
canEdit && ( | |||
<div className="pull-right"> | |||
<ModalButton modal={this.renderConditionModal}> | |||
{({ onClick }) => ( | |||
<Button data-test="quality-gates__add-condition" onClick={onClick}> | |||
{translate('quality_gates.add_condition')} | |||
</Button> | |||
)} | |||
</ModalButton> | |||
</div> | |||
)} | |||
{(qualityGate.caycStatus === CaycStatus.NonCompliant || unlockEditing) && canEdit && ( | |||
<div className="pull-right"> | |||
<ModalButton modal={this.renderConditionModal}> | |||
{({ onClick }) => ( | |||
<Button data-test="quality-gates__add-condition" onClick={onClick}> | |||
{translate('quality_gates.add_condition')} | |||
</Button> | |||
)} | |||
</ModalButton> | |||
</div> | |||
)} | |||
<header className="display-flex-center"> | |||
<h2 className="big">{translate('quality_gates.conditions')}</h2> | |||
@@ -314,7 +341,7 @@ export class Conditions extends React.PureComponent<Props, State> { | |||
</div> | |||
)} | |||
{qualityGate.isCaycCompliant && !unlockEditing && canEdit && ( | |||
{qualityGate.caycStatus !== CaycStatus.NonCompliant && !unlockEditing && canEdit && ( | |||
<div className="big-spacer-top big-spacer-bottom cayc-warning-description it__qg-unfollow-cayc"> | |||
<p> | |||
<FormattedMessage |
@@ -86,7 +86,7 @@ export default class Details extends React.PureComponent<Props, State> { | |||
addGlobalSuccessMessage(translate('quality_gates.condition_added')); | |||
const updatedQualityGate = addCondition(clone(qualityGate), condition); | |||
if (qualityGate.isCaycCompliant !== updatedQualityGate.isCaycCompliant) { | |||
if (qualityGate.caycStatus !== updatedQualityGate.caycStatus) { | |||
this.props.refreshQualityGates(); | |||
} | |||
@@ -104,7 +104,7 @@ export default class Details extends React.PureComponent<Props, State> { | |||
} | |||
addGlobalSuccessMessage(translate('quality_gates.condition_updated')); | |||
const updatedQualityGate = replaceCondition(clone(qualityGate), newCondition, oldCondition); | |||
if (qualityGate.isCaycCompliant !== updatedQualityGate.isCaycCompliant) { | |||
if (qualityGate.caycStatus !== updatedQualityGate.caycStatus) { | |||
this.props.refreshQualityGates(); | |||
} | |||
return { | |||
@@ -121,7 +121,7 @@ export default class Details extends React.PureComponent<Props, State> { | |||
} | |||
addGlobalSuccessMessage(translate('quality_gates.condition_deleted')); | |||
const updatedQualityGate = deleteCondition(clone(qualityGate), condition); | |||
if (qualityGate.isCaycCompliant !== updatedQualityGate.isCaycCompliant) { | |||
if (qualityGate.caycStatus !== updatedQualityGate.caycStatus) { | |||
this.props.refreshQualityGates(); | |||
} | |||
return { |
@@ -24,7 +24,7 @@ import ModalButton from '../../../components/controls/ModalButton'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import AlertWarnIcon from '../../../components/icons/AlertWarnIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { QualityGate } from '../../../types/types'; | |||
import { CaycStatus, QualityGate } from '../../../types/types'; | |||
import BuiltInQualityGateBadge from './BuiltInQualityGateBadge'; | |||
import CaycBadgeTooltip from './CaycBadgeTooltip'; | |||
import CopyQualityGateForm from './CopyQualityGateForm'; | |||
@@ -64,7 +64,6 @@ export default class DetailsHeader extends React.PureComponent<Props> { | |||
render() { | |||
const { qualityGate } = this.props; | |||
const actions = qualityGate.actions || ({} as any); | |||
const { isCaycCompliant } = qualityGate; | |||
return ( | |||
<div className="layout-page-header-panel layout-page-main-header issues-main-header"> | |||
@@ -73,7 +72,7 @@ export default class DetailsHeader extends React.PureComponent<Props> { | |||
<div className="pull-left display-flex-center"> | |||
<h2>{qualityGate.name}</h2> | |||
{qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="spacer-left" />} | |||
{!qualityGate.isCaycCompliant && ( | |||
{qualityGate.caycStatus === CaycStatus.NonCompliant && ( | |||
<Tooltip overlay={<CaycBadgeTooltip />} mouseLeaveDelay={TOOLTIP_MOUSE_LEAVE_DELAY}> | |||
<AlertWarnIcon className="spacer-left" /> | |||
</Tooltip> | |||
@@ -111,7 +110,9 @@ export default class DetailsHeader extends React.PureComponent<Props> { | |||
{({ onClick }) => ( | |||
<Tooltip | |||
overlay={ | |||
!isCaycCompliant ? translate('quality_gates.cannot_copy_no_cayc') : null | |||
qualityGate.caycStatus === CaycStatus.NonCompliant | |||
? translate('quality_gates.cannot_copy_no_cayc') | |||
: null | |||
} | |||
accessible={false} | |||
> | |||
@@ -119,7 +120,7 @@ export default class DetailsHeader extends React.PureComponent<Props> { | |||
className="little-spacer-left" | |||
id="quality-gate-copy" | |||
onClick={onClick} | |||
disabled={!isCaycCompliant} | |||
disabled={qualityGate.caycStatus === CaycStatus.NonCompliant} | |||
> | |||
{translate('copy')} | |||
</Button> | |||
@@ -130,13 +131,15 @@ export default class DetailsHeader extends React.PureComponent<Props> { | |||
{actions.setAsDefault && ( | |||
<Tooltip | |||
overlay={ | |||
!isCaycCompliant ? translate('quality_gates.cannot_set_default_no_cayc') : null | |||
qualityGate.caycStatus === CaycStatus.NonCompliant | |||
? translate('quality_gates.cannot_set_default_no_cayc') | |||
: null | |||
} | |||
accessible={false} | |||
> | |||
<Button | |||
className="little-spacer-left" | |||
disabled={!isCaycCompliant} | |||
disabled={qualityGate.caycStatus === CaycStatus.NonCompliant} | |||
id="quality-gate-toggle-default" | |||
onClick={this.handleSetAsDefaultClick} | |||
> |
@@ -23,7 +23,7 @@ import Tooltip from '../../../components/controls/Tooltip'; | |||
import AlertWarnIcon from '../../../components/icons/AlertWarnIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getQualityGateUrl } from '../../../helpers/urls'; | |||
import { QualityGate } from '../../../types/types'; | |||
import { CaycStatus, QualityGate } from '../../../types/types'; | |||
import BuiltInQualityGateBadge from './BuiltInQualityGateBadge'; | |||
interface Props { | |||
@@ -49,7 +49,7 @@ export default function List({ qualityGates, currentQualityGate }: Props) { | |||
)} | |||
{qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="little-spacer-left" />} | |||
{!qualityGate.isCaycCompliant && ( | |||
{qualityGate.caycStatus === CaycStatus.NonCompliant && ( | |||
<> | |||
{/* Adding a11y-hidden span for accessibility */} | |||
<span className="a11y-hidden">{translate('quality_gates.cayc.tooltip.message')}</span> |
@@ -340,7 +340,10 @@ it('should show warning banner when CAYC condition is not properly set and shoul | |||
expect(conditionsWrapper.getAllByText('Coverage')).toHaveLength(2); // This quality gate has duplicate condition | |||
expect(conditionsWrapper.getByText('Duplicated Lines (%)')).toBeInTheDocument(); | |||
expect(screen.queryByTestId('quality-gates__conditions-overall')).not.toBeInTheDocument(); | |||
const overallConditionsWrapper = within( | |||
await screen.findByTestId('quality-gates__conditions-overall') | |||
); | |||
expect(overallConditionsWrapper.getByText('Complexity / Function')).toBeInTheDocument(); | |||
}); | |||
it('should show success banner when quality gate is CAYC compliant', async () => { | |||
@@ -371,6 +374,27 @@ it('should show success banner when quality gate is CAYC compliant', async () => | |||
expect(await conditionsWrapper.findByText('Duplicated Lines (%)')).toBeInTheDocument(); | |||
}); | |||
it('should show info banner when quality gate is CAYC over-compliant', async () => { | |||
const user = userEvent.setup(); | |||
handler.setIsAdmin(true); | |||
renderQualityGateApp(); | |||
const qualityGate = await screen.findByText('Over Compliant CAYC QG'); | |||
await user.click(qualityGate); | |||
expect(screen.getByText('quality_gates.cayc.banner.title')).toBeInTheDocument(); | |||
expect(screen.getByText('quality_gates.cayc.banner.description')).toBeInTheDocument(); | |||
expect(screen.getByText('quality_gates.cayc_over_compliant.banner.title')).toBeInTheDocument(); | |||
expect( | |||
screen.queryByText('quality_gates.cayc_condition.missing_warning.title') | |||
).not.toBeInTheDocument(); | |||
expect( | |||
screen.queryByRole('button', { name: 'quality_gates.cayc_condition.review_update' }) | |||
).not.toBeInTheDocument(); | |||
expect(screen.getByText('quality_gates.cayc.unlock_edit')).toBeInTheDocument(); | |||
}); | |||
it('should unlock editing option for CAYC conditions', async () => { | |||
const user = userEvent.setup(); | |||
handler.setIsAdmin(true); |
@@ -73,6 +73,22 @@ | |||
color: var(--alertTextSuccess); | |||
} | |||
.cayc-over-compliant-header { | |||
color: var(--veryDarkBlue); | |||
} | |||
.cayc-warning-description { | |||
line-height: 18px; | |||
} | |||
.red-text { | |||
color: var(--red); | |||
} | |||
.green-text { | |||
color: var(--success500); | |||
} | |||
.strike-through { | |||
text-decoration: line-through; | |||
} |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import { getLocalizedMetricName } from '../../helpers/l10n'; | |||
import { isDiffMetric } from '../../helpers/measures'; | |||
import { Condition, Dict, Metric, QualityGate } from '../../types/types'; | |||
import { CaycStatus, Condition, Dict, Metric, QualityGate } from '../../types/types'; | |||
const CAYC_CONDITIONS: { [key: string]: Condition } = { | |||
new_reliability_rating: { | |||
@@ -61,6 +61,12 @@ const CAYC_CONDITIONS: { [key: string]: Condition } = { | |||
}; | |||
export const CAYC_CONDITIONS_WITHOUT_FIXED_VALUE = ['new_duplicated_lines_density', 'new_coverage']; | |||
export const CAYC_CONDITIONS_WITH_FIXED_VALUE = [ | |||
'new_security_hotspots_reviewed', | |||
'new_maintainability_rating', | |||
'new_security_rating', | |||
'new_reliability_rating', | |||
]; | |||
export function isCaycCondition(condition: Condition) { | |||
return condition.metric in CAYC_CONDITIONS; | |||
@@ -70,11 +76,9 @@ export function getWeakMissingAndNonCaycConditions(conditions: Condition[]) { | |||
const result: { | |||
weakConditions: Condition[]; | |||
missingConditions: Condition[]; | |||
nonCaycConditions: Condition[]; | |||
} = { | |||
weakConditions: [], | |||
missingConditions: [], | |||
nonCaycConditions: [], | |||
}; | |||
Object.keys(CAYC_CONDITIONS).forEach((key) => { | |||
const selectedCondition = conditions.find((condition) => condition.metric === key); | |||
@@ -87,8 +91,6 @@ export function getWeakMissingAndNonCaycConditions(conditions: Condition[]) { | |||
result.weakConditions.push(selectedCondition); | |||
} | |||
}); | |||
result.nonCaycConditions = getNonCaycConditions(conditions); | |||
return result; | |||
} | |||
@@ -102,10 +104,6 @@ export function getCaycConditionsWithCorrectValue(conditions: Condition[]) { | |||
}); | |||
} | |||
export function getNonCaycConditions(conditions: Condition[]) { | |||
return conditions.filter((condition) => !isCaycCondition(condition)); | |||
} | |||
export function getCorrectCaycCondition(condition: Condition) { | |||
if (CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(condition.metric)) { | |||
return condition; | |||
@@ -121,7 +119,7 @@ export function addCondition(qualityGate: QualityGate, condition: Condition): Qu | |||
const oldConditions = qualityGate.conditions || []; | |||
const conditions = [...oldConditions, condition]; | |||
if (conditions) { | |||
qualityGate.isCaycCompliant = updateCaycComplaintStatus(conditions); | |||
qualityGate.caycStatus = updateCaycCompliantStatus(conditions); | |||
} | |||
return { ...qualityGate, conditions }; | |||
} | |||
@@ -130,7 +128,7 @@ export function deleteCondition(qualityGate: QualityGate, condition: Condition): | |||
const conditions = | |||
qualityGate.conditions && qualityGate.conditions.filter((candidate) => candidate !== condition); | |||
if (conditions) { | |||
qualityGate.isCaycCompliant = updateCaycComplaintStatus(conditions); | |||
qualityGate.caycStatus = updateCaycCompliantStatus(conditions); | |||
} | |||
return { ...qualityGate, conditions }; | |||
} | |||
@@ -146,21 +144,21 @@ export function replaceCondition( | |||
return candidate === oldCondition ? newCondition : candidate; | |||
}); | |||
if (conditions) { | |||
qualityGate.isCaycCompliant = updateCaycComplaintStatus(conditions); | |||
qualityGate.caycStatus = updateCaycCompliantStatus(conditions); | |||
} | |||
return { ...qualityGate, conditions }; | |||
} | |||
export function updateCaycComplaintStatus(conditions: Condition[]) { | |||
if (conditions.length !== Object.keys(CAYC_CONDITIONS).length) { | |||
return false; | |||
export function updateCaycCompliantStatus(conditions: Condition[]) { | |||
if (conditions.length < Object.keys(CAYC_CONDITIONS).length) { | |||
return CaycStatus.NonCompliant; | |||
} | |||
for (const key of Object.keys(CAYC_CONDITIONS)) { | |||
const selectedCondition = conditions.find((condition) => condition.metric === key); | |||
if (!selectedCondition) { | |||
return false; | |||
return CaycStatus.NonCompliant; | |||
} | |||
if ( | |||
@@ -168,11 +166,15 @@ export function updateCaycComplaintStatus(conditions: Condition[]) { | |||
selectedCondition && | |||
selectedCondition.error !== CAYC_CONDITIONS[key].error | |||
) { | |||
return false; | |||
return CaycStatus.NonCompliant; | |||
} | |||
} | |||
return true; | |||
if (conditions.length > Object.keys(CAYC_CONDITIONS).length) { | |||
return CaycStatus.OverCompliant; | |||
} | |||
return CaycStatus.Compliant; | |||
} | |||
export function getPossibleOperators(metric: Metric) { |
@@ -24,7 +24,7 @@ import { | |||
QualityGateStatusCondition, | |||
QualityGateStatusConditionEnhanced, | |||
} from '../../types/quality-gates'; | |||
import { QualityGate } from '../../types/types'; | |||
import { CaycStatus, QualityGate } from '../../types/types'; | |||
import { mockMeasureEnhanced, mockMetric } from '../testMocks'; | |||
export function mockQualityGate(overrides: Partial<QualityGate> = {}): QualityGate { | |||
@@ -39,7 +39,7 @@ export function mockQualityGateStatus( | |||
): QualityGateStatus { | |||
return { | |||
ignoredConditions: false, | |||
isCaycCompliant: true, | |||
caycStatus: CaycStatus.Compliant, | |||
failedConditions: [mockQualityGateStatusConditionEnhanced()], | |||
key: 'foo', | |||
name: 'Foo', | |||
@@ -90,7 +90,7 @@ export function mockQualityGateProjectStatus( | |||
}, | |||
], | |||
ignoredConditions: false, | |||
isCaycCompliant: true, | |||
caycStatus: CaycStatus.Compliant, | |||
status: 'OK', | |||
...overrides, | |||
}; | |||
@@ -122,7 +122,7 @@ export function mockQualityGateApplicationStatus( | |||
value: '5', | |||
}, | |||
], | |||
isCaycCompliant: true, | |||
caycStatus: CaycStatus.Compliant, | |||
status: 'ERROR', | |||
}, | |||
{ | |||
@@ -138,7 +138,7 @@ export function mockQualityGateApplicationStatus( | |||
value: '15', | |||
}, | |||
], | |||
isCaycCompliant: true, | |||
caycStatus: CaycStatus.Compliant, | |||
status: 'ERROR', | |||
}, | |||
], |
@@ -18,14 +18,14 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { BranchLike } from './branch-like'; | |||
import { MeasureEnhanced, Metric, Status } from './types'; | |||
import { CaycStatus, MeasureEnhanced, Metric, Status } from './types'; | |||
import { UserBase } from './users'; | |||
export interface QualityGateProjectStatus { | |||
conditions?: QualityGateProjectStatusCondition[]; | |||
ignoredConditions: boolean; | |||
status: Status; | |||
isCaycCompliant: boolean; | |||
caycStatus: CaycStatus; | |||
} | |||
export interface QualityGateProjectStatusCondition { | |||
@@ -59,13 +59,13 @@ export interface QualityGateApplicationStatusChildProject { | |||
key: string; | |||
name: string; | |||
status: Status; | |||
isCaycCompliant: boolean; | |||
caycStatus: CaycStatus; | |||
} | |||
export interface QualityGateStatus { | |||
failedConditions: QualityGateStatusConditionEnhanced[]; | |||
ignoredConditions?: boolean; | |||
isCaycCompliant: boolean; | |||
caycStatus: CaycStatus; | |||
key: string; | |||
name: string; | |||
status: Status; |
@@ -506,6 +506,12 @@ export interface ProjectLink { | |||
url: string; | |||
} | |||
export enum CaycStatus { | |||
Compliant = 'compliant', | |||
NonCompliant = 'non-compliant', | |||
OverCompliant = 'over-compliant', | |||
} | |||
export interface QualityGate { | |||
actions?: { | |||
associateProjects?: boolean; | |||
@@ -518,7 +524,7 @@ export interface QualityGate { | |||
}; | |||
conditions?: Condition[]; | |||
isBuiltIn?: boolean; | |||
isCaycCompliant?: boolean; | |||
caycStatus?: CaycStatus; | |||
isDefault?: boolean; | |||
name: string; | |||
} |
@@ -241,7 +241,7 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader { | |||
for (QualityGateDto qualityGateDto : qualityGateDtos) { | |||
qualityGates.add( | |||
new TelemetryData.QualityGate(qualityGateDto.getUuid(), qualityGateCaycChecker.checkCaycCompliant(dbSession, | |||
qualityGateDto.getUuid())) | |||
qualityGateDto.getUuid()).toString()) | |||
); | |||
} | |||
@@ -63,6 +63,7 @@ import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.entry; | |||
import static org.assertj.core.groups.Tuple.tuple; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.spy; | |||
import static org.mockito.Mockito.when; | |||
@@ -83,6 +84,7 @@ import static org.sonar.core.platform.EditionProvider.Edition.ENTERPRISE; | |||
import static org.sonar.db.component.BranchType.BRANCH; | |||
import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_CPP_KEY; | |||
import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_C_KEY; | |||
import static org.sonar.server.qualitygate.QualityGateCaycStatus.NON_COMPLIANT; | |||
import static org.sonar.server.telemetry.TelemetryDataLoaderImpl.SCIM_PROPERTY_ENABLED; | |||
@RunWith(DataProviderRunner.class) | |||
@@ -118,6 +120,7 @@ public class TelemetryDataLoaderImplTest { | |||
public void setUpBuiltInQualityGate() { | |||
String builtInQgName = "Sonar Way"; | |||
builtInDefaultQualityGate = db.qualityGates().insertQualityGate(qg -> qg.setName(builtInQgName).setBuiltIn(true)); | |||
when(qualityGateCaycChecker.checkCaycCompliant(any(), any())).thenReturn(NON_COMPLIANT); | |||
db.qualityGates().setDefaultQualityGate(builtInDefaultQualityGate); | |||
bugsDto = db.measures().insertMetric(m -> m.setKey(BUGS_KEY)); | |||
@@ -229,11 +232,11 @@ public class TelemetryDataLoaderImplTest { | |||
tuple(1L, 0L, qualityGate1.getUuid(), "scm-1", "ci-1", "azure_devops_cloud", Optional.of(1L), Optional.of(1L), Optional.of(1L), Optional.of(50L), Optional.of(5L)), | |||
tuple(1L, 0L, builtInDefaultQualityGate.getUuid(), "scm-2", "ci-2", "github_cloud", Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty())); | |||
assertThat(data.getQualityGates()) | |||
.extracting(TelemetryData.QualityGate::uuid, TelemetryData.QualityGate::isCaycCompliant) | |||
.extracting(TelemetryData.QualityGate::uuid, TelemetryData.QualityGate::caycStatus) | |||
.containsExactlyInAnyOrder( | |||
tuple(builtInDefaultQualityGate.getUuid(), false), | |||
tuple(qualityGate1.getUuid(), false), | |||
tuple(qualityGate2.getUuid(), false) | |||
tuple(builtInDefaultQualityGate.getUuid(), "non-compliant"), | |||
tuple(qualityGate1.getUuid(), "non-compliant"), | |||
tuple(qualityGate2.getUuid(), "non-compliant") | |||
); | |||
} | |||
@@ -40,6 +40,9 @@ import static org.sonar.api.measures.CoreMetrics.NEW_RELIABILITY_RATING; | |||
import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED; | |||
import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_RATING; | |||
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; | |||
import static org.sonar.server.qualitygate.QualityGateCaycStatus.COMPLIANT; | |||
import static org.sonar.server.qualitygate.QualityGateCaycStatus.NON_COMPLIANT; | |||
import static org.sonar.server.qualitygate.QualityGateCaycStatus.OVER_COMPLIANT; | |||
public class QualityGateCaycChecker { | |||
@@ -67,13 +70,13 @@ public class QualityGateCaycChecker { | |||
this.dbClient = dbClient; | |||
} | |||
public boolean checkCaycCompliant(DbSession dbSession, String qualityGateUuid) { | |||
public QualityGateCaycStatus checkCaycCompliant(DbSession dbSession, String qualityGateUuid) { | |||
var conditionsByMetricId = dbClient.gateConditionDao().selectForQualityGate(dbSession, qualityGateUuid) | |||
.stream() | |||
.collect(uniqueIndex(QualityGateConditionDto::getMetricUuid)); | |||
if (conditionsByMetricId.size() != CAYC_METRICS.size()) { | |||
return false; | |||
if (conditionsByMetricId.size() < CAYC_METRICS.size()) { | |||
return NON_COMPLIANT; | |||
} | |||
var metrics = dbClient.metricDao().selectByUuids(dbSession, conditionsByMetricId.keySet()) | |||
@@ -85,14 +88,20 @@ public class QualityGateCaycChecker { | |||
.filter(metric -> checkMetricCaycCompliant(conditionsByMetricId.get(metric.getUuid()), metric)) | |||
.count(); | |||
return count == CAYC_METRICS.size(); | |||
if (metrics.size() == count && count == CAYC_METRICS.size()) { | |||
return COMPLIANT; | |||
} else if (metrics.size() > count && count == CAYC_METRICS.size()) { | |||
return OVER_COMPLIANT; | |||
} | |||
return NON_COMPLIANT; | |||
} | |||
public boolean checkCaycCompliantFromProject(DbSession dbSession, String projectUuid) { | |||
public QualityGateCaycStatus checkCaycCompliantFromProject(DbSession dbSession, String projectUuid) { | |||
return Optional.ofNullable(dbClient.qualityGateDao().selectByProjectUuid(dbSession, projectUuid)) | |||
.or(() -> Optional.ofNullable(dbClient.qualityGateDao().selectDefault(dbSession))) | |||
.map(qualityGate -> checkCaycCompliant(dbSession, qualityGate.getUuid())) | |||
.orElse(false); | |||
.orElse(NON_COMPLIANT); | |||
} | |||
private static boolean checkMetricCaycCompliant(QualityGateConditionDto condition, MetricDto metric) { |
@@ -0,0 +1,38 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.qualitygate; | |||
public enum QualityGateCaycStatus { | |||
NON_COMPLIANT("non-compliant"), | |||
COMPLIANT("compliant"), | |||
OVER_COMPLIANT("over-compliant"); | |||
final String status; | |||
QualityGateCaycStatus(String status) { | |||
this.status = status; | |||
} | |||
@Override | |||
public String toString() { | |||
return status; | |||
} | |||
} |
@@ -59,7 +59,7 @@ public class ListAction implements QualityGatesWsAction { | |||
.setSince("4.3") | |||
.setResponseExample(Resources.getResource(this.getClass(), "list-example.json")) | |||
.setChangelog( | |||
new Change("9.9", "'isCaycCompliant' field is added on quality gate"), | |||
new Change("9.9", "'caycStatus' field is added on quality gate"), | |||
new Change("8.4", "Field 'id' in the response is deprecated. Format changes from integer to string."), | |||
new Change("7.0", "'isDefault' field is added on quality gate"), | |||
new Change("7.0", "'default' field on root level is deprecated"), | |||
@@ -88,7 +88,7 @@ public class ListAction implements QualityGatesWsAction { | |||
.setName(qualityGate.getName()) | |||
.setIsDefault(qualityGate.getUuid().equals(defaultUuid)) | |||
.setIsBuiltIn(qualityGate.isBuiltIn()) | |||
.setIsCaycCompliant(qualityGateCaycChecker.checkCaycCompliant(dbSession, qualityGate.getUuid())) | |||
.setCaycStatus(qualityGateCaycChecker.checkCaycCompliant(dbSession, qualityGate.getUuid()).toString()) | |||
.setActions(wsSupport.getActions(dbSession, qualityGate, defaultQualityGate)) | |||
.build()) | |||
.collect(toList())); |
@@ -42,6 +42,7 @@ import org.sonar.db.project.ProjectDto; | |||
import org.sonar.server.component.ComponentFinder; | |||
import org.sonar.server.exceptions.BadRequestException; | |||
import org.sonar.server.qualitygate.QualityGateCaycChecker; | |||
import org.sonar.server.qualitygate.QualityGateCaycStatus; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonar.server.ws.KeyExamples; | |||
import org.sonarqube.ws.Qualitygates.ProjectStatusResponse; | |||
@@ -95,7 +96,7 @@ public class ProjectStatusAction implements QualityGatesWsAction { | |||
.setSince("5.3") | |||
.setHandler(this) | |||
.setChangelog( | |||
new Change("9.9", "'isCaycCompliant' field is added to the response"), | |||
new Change("9.9", "'caycStatus' field is added to the response"), | |||
new Change("9.5", "The 'Execute Analysis' permission also allows to access the endpoint"), | |||
new Change("8.5", "The field 'periods' in the response is deprecated. Use 'period' instead"), | |||
new Change("7.7", "The parameters 'branch' and 'pullRequest' were added"), | |||
@@ -152,10 +153,10 @@ public class ProjectStatusAction implements QualityGatesWsAction { | |||
ProjectAndSnapshot projectAndSnapshot = getProjectAndSnapshot(dbSession, analysisId, projectUuid, projectKey, branchKey, pullRequestId); | |||
checkPermission(projectAndSnapshot.project); | |||
Optional<String> measureData = loadQualityGateDetails(dbSession, projectAndSnapshot, analysisId != null); | |||
boolean isCaycCompliant = qualityGateCaycChecker.checkCaycCompliantFromProject(dbSession, projectAndSnapshot.project.getUuid()); | |||
QualityGateCaycStatus caycStatus = qualityGateCaycChecker.checkCaycCompliantFromProject(dbSession, projectAndSnapshot.project.getUuid()); | |||
return ProjectStatusResponse.newBuilder() | |||
.setProjectStatus(new QualityGateDetailsFormatter(measureData.orElse(null), projectAndSnapshot.snapshotDto.orElse(null), isCaycCompliant).format()) | |||
.setProjectStatus(new QualityGateDetailsFormatter(measureData.orElse(null), projectAndSnapshot.snapshotDto.orElse(null), caycStatus).format()) | |||
.build(); | |||
} | |||
@@ -28,6 +28,7 @@ import java.util.function.Predicate; | |||
import java.util.stream.StreamSupport; | |||
import javax.annotation.Nullable; | |||
import org.sonar.db.component.SnapshotDto; | |||
import org.sonar.server.qualitygate.QualityGateCaycStatus; | |||
import org.sonarqube.ws.Qualitygates.ProjectStatusResponse; | |||
import org.sonarqube.ws.Qualitygates.ProjectStatusResponse.NewCodePeriod; | |||
import org.sonarqube.ws.Qualitygates.ProjectStatusResponse.Period; | |||
@@ -38,13 +39,13 @@ import static org.sonar.api.utils.DateUtils.formatDateTime; | |||
public class QualityGateDetailsFormatter { | |||
private final Optional<String> optionalMeasureData; | |||
private final Optional<SnapshotDto> optionalSnapshot; | |||
private final boolean isCaycCompliant; | |||
private final QualityGateCaycStatus caycStatus; | |||
private final ProjectStatusResponse.ProjectStatus.Builder projectStatusBuilder; | |||
public QualityGateDetailsFormatter(@Nullable String measureData, @Nullable SnapshotDto snapshot, boolean isCaycCompliant) { | |||
public QualityGateDetailsFormatter(@Nullable String measureData, @Nullable SnapshotDto snapshot, QualityGateCaycStatus caycStatus) { | |||
this.optionalMeasureData = Optional.ofNullable(measureData); | |||
this.optionalSnapshot = Optional.ofNullable(snapshot); | |||
this.isCaycCompliant = isCaycCompliant; | |||
this.caycStatus = caycStatus; | |||
this.projectStatusBuilder = ProjectStatusResponse.ProjectStatus.newBuilder(); | |||
} | |||
@@ -57,7 +58,7 @@ public class QualityGateDetailsFormatter { | |||
ProjectStatusResponse.Status qualityGateStatus = measureLevelToQualityGateStatus(json.get("level").getAsString()); | |||
projectStatusBuilder.setStatus(qualityGateStatus); | |||
projectStatusBuilder.setIsCaycCompliant(isCaycCompliant); | |||
projectStatusBuilder.setCaycStatus(caycStatus.toString()); | |||
formatIgnoredConditions(json); | |||
formatConditions(json.getAsJsonArray("conditions")); | |||
@@ -204,7 +205,7 @@ public class QualityGateDetailsFormatter { | |||
} | |||
private ProjectStatusResponse.ProjectStatus newResponseWithoutQualityGateDetails() { | |||
return ProjectStatusResponse.ProjectStatus.newBuilder().setStatus(ProjectStatusResponse.Status.NONE).setIsCaycCompliant(isCaycCompliant).build(); | |||
return ProjectStatusResponse.ProjectStatus.newBuilder().setStatus(ProjectStatusResponse.Status.NONE).setCaycStatus(caycStatus.toString()).build(); | |||
} | |||
private static Predicate<JsonObject> isConditionOnValidPeriod() { |
@@ -35,6 +35,7 @@ import org.sonar.db.metric.MetricDto; | |||
import org.sonar.db.qualitygate.QualityGateConditionDto; | |||
import org.sonar.db.qualitygate.QualityGateDto; | |||
import org.sonar.server.qualitygate.QualityGateCaycChecker; | |||
import org.sonar.server.qualitygate.QualityGateCaycStatus; | |||
import org.sonar.server.qualitygate.QualityGateFinder; | |||
import org.sonarqube.ws.Qualitygates.ShowWsResponse; | |||
@@ -71,7 +72,7 @@ public class ShowAction implements QualityGatesWsAction { | |||
.setSince("4.3") | |||
.setResponseExample(Resources.getResource(this.getClass(), "show-example.json")) | |||
.setChangelog( | |||
new Change("9.9", "'isCaycCompliant' field is added to the response"), | |||
new Change("9.9", "'caycStatus' field is added to the response"), | |||
new Change("8.4", "Parameter 'id' is deprecated. Format changes from integer to string. Use 'name' instead."), | |||
new Change("8.4", "Field 'id' in the response is deprecated."), | |||
new Change("7.6", "'period' and 'warning' fields of conditions are removed from the response"), | |||
@@ -99,8 +100,8 @@ public class ShowAction implements QualityGatesWsAction { | |||
Collection<QualityGateConditionDto> conditions = getConditions(dbSession, qualityGate); | |||
Map<String, MetricDto> metricsByUuid = getMetricsByUuid(dbSession, conditions); | |||
QualityGateDto defaultQualityGate = qualityGateFinder.getDefault(dbSession); | |||
boolean isCaycCompliant = qualityGateCaycChecker.checkCaycCompliant(dbSession, qualityGate.getUuid()); | |||
writeProtobuf(buildResponse(dbSession, qualityGate, defaultQualityGate, conditions, metricsByUuid, isCaycCompliant), request, response); | |||
QualityGateCaycStatus caycStatus = qualityGateCaycChecker.checkCaycCompliant(dbSession, qualityGate.getUuid()); | |||
writeProtobuf(buildResponse(dbSession, qualityGate, defaultQualityGate, conditions, metricsByUuid, caycStatus), request, response); | |||
} | |||
} | |||
@@ -124,12 +125,12 @@ public class ShowAction implements QualityGatesWsAction { | |||
} | |||
private ShowWsResponse buildResponse(DbSession dbSession, QualityGateDto qualityGate, QualityGateDto defaultQualityGate, Collection<QualityGateConditionDto> conditions, | |||
Map<String, MetricDto> metricsByUuid, boolean isCaycCompliant) { | |||
Map<String, MetricDto> metricsByUuid, QualityGateCaycStatus caycStatus) { | |||
return ShowWsResponse.newBuilder() | |||
.setId(qualityGate.getUuid()) | |||
.setName(qualityGate.getName()) | |||
.setIsBuiltIn(qualityGate.isBuiltIn()) | |||
.setIsCaycCompliant(isCaycCompliant) | |||
.setCaycStatus(caycStatus.toString()) | |||
.addAllConditions(conditions.stream() | |||
.map(toWsCondition(metricsByUuid)) | |||
.collect(toList())) |
@@ -2,7 +2,7 @@ | |||
"projectStatus": { | |||
"status": "ERROR", | |||
"ignoredConditions": false, | |||
"isCaycCompliant": false, | |||
"caycStatus": "non-compliant", | |||
"conditions": [ | |||
{ | |||
"status": "ERROR", |
@@ -30,7 +30,7 @@ import org.sonar.db.DbTester; | |||
import org.sonar.db.metric.MetricDto; | |||
import org.sonar.db.qualitygate.QualityGateConditionDto; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES; | |||
import static org.sonar.api.measures.CoreMetrics.LINE_COVERAGE; | |||
import static org.sonar.api.measures.CoreMetrics.NEW_COVERAGE; | |||
@@ -40,6 +40,9 @@ import static org.sonar.api.measures.CoreMetrics.NEW_RELIABILITY_RATING; | |||
import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED; | |||
import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_RATING; | |||
import static org.sonar.server.qualitygate.QualityGateCaycChecker.CAYC_METRICS; | |||
import static org.sonar.server.qualitygate.QualityGateCaycStatus.COMPLIANT; | |||
import static org.sonar.server.qualitygate.QualityGateCaycStatus.NON_COMPLIANT; | |||
import static org.sonar.server.qualitygate.QualityGateCaycStatus.OVER_COMPLIANT; | |||
public class QualityGateCaycCheckerTest { | |||
@@ -51,7 +54,7 @@ public class QualityGateCaycCheckerTest { | |||
public void checkCaycCompliant() { | |||
String qualityGateUuid = "abcd"; | |||
CAYC_METRICS.forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue())); | |||
assertThat(underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)).isTrue(); | |||
assertEquals(COMPLIANT, underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)); | |||
} | |||
@Test | |||
@@ -62,7 +65,7 @@ public class QualityGateCaycCheckerTest { | |||
// extra conditions outside of CAYC requirements | |||
List.of(LINE_COVERAGE, DUPLICATED_LINES).forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue())); | |||
assertThat(underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)).isFalse(); | |||
assertEquals(OVER_COMPLIANT, underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)); | |||
} | |||
@Test | |||
@@ -75,7 +78,7 @@ public class QualityGateCaycCheckerTest { | |||
var metric = metrics.get(i); | |||
insertCondition(metric, qualityGateUuid, idx == i ? metric.getWorstValue() : metric.getBestValue()); | |||
} | |||
assertThat(underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)).isFalse(); | |||
assertEquals(NON_COMPLIANT, underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)); | |||
}); | |||
} | |||
@@ -84,7 +87,7 @@ public class QualityGateCaycCheckerTest { | |||
String qualityGateUuid = "abcd"; | |||
List.of(NEW_MAINTAINABILITY_RATING, NEW_RELIABILITY_RATING, NEW_SECURITY_HOTSPOTS_REVIEWED, NEW_DUPLICATED_LINES_DENSITY) | |||
.forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue())); | |||
assertThat(underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)).isFalse(); | |||
assertEquals(NON_COMPLIANT, underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)); | |||
} | |||
@Test | |||
@@ -94,7 +97,7 @@ public class QualityGateCaycCheckerTest { | |||
.forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue())); | |||
List.of(NEW_COVERAGE, NEW_DUPLICATED_LINES_DENSITY) | |||
.forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getWorstValue())); | |||
assertThat(underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)).isTrue(); | |||
assertEquals(COMPLIANT, underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)); | |||
} | |||
private void insertCondition(MetricDto metricDto, String qualityGateUuid, Double threshold) { |
@@ -19,6 +19,7 @@ | |||
*/ | |||
package org.sonar.server.qualitygate.ws; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.api.server.ws.WebService; | |||
@@ -43,6 +44,9 @@ import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_GATES; | |||
import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES; | |||
import static org.sonar.server.qualitygate.QualityGateCaycStatus.COMPLIANT; | |||
import static org.sonar.server.qualitygate.QualityGateCaycStatus.NON_COMPLIANT; | |||
import static org.sonar.server.qualitygate.QualityGateCaycStatus.OVER_COMPLIANT; | |||
import static org.sonar.test.JsonAssert.assertJson; | |||
import static org.sonarqube.ws.Qualitygates.ListWsResponse; | |||
@@ -61,6 +65,11 @@ public class ListActionTest { | |||
private final WsActionTester ws = new WsActionTester(new ListAction(db.getDbClient(), | |||
new QualityGatesWsSupport(dbClient, userSession, TestComponentFinder.from(db)), qualityGateFinder, qualityGateCaycChecker)); | |||
@Before | |||
public void setUp() { | |||
when(qualityGateCaycChecker.checkCaycCompliant(any(), any())).thenReturn(COMPLIANT); | |||
} | |||
@Test | |||
public void list_quality_gates() { | |||
QualityGateDto defaultQualityGate = db.qualityGates().insertQualityGate(); | |||
@@ -94,11 +103,13 @@ public class ListActionTest { | |||
} | |||
@Test | |||
public void test_isCaycCompliant_flag() { | |||
public void test_caycStatus_flag() { | |||
QualityGateDto qualityGate1 = db.qualityGates().insertQualityGate(); | |||
QualityGateDto qualityGate2 = db.qualityGates().insertQualityGate(); | |||
when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate1.getUuid()))).thenReturn(true); | |||
when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate2.getUuid()))).thenReturn(false); | |||
QualityGateDto qualityGate3 = db.qualityGates().insertQualityGate(); | |||
when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate1.getUuid()))).thenReturn(COMPLIANT); | |||
when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate2.getUuid()))).thenReturn(NON_COMPLIANT); | |||
when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate3.getUuid()))).thenReturn(OVER_COMPLIANT); | |||
db.qualityGates().setDefaultQualityGate(qualityGate1); | |||
@@ -106,10 +117,11 @@ public class ListActionTest { | |||
.executeProtobuf(ListWsResponse.class); | |||
assertThat(response.getQualitygatesList()) | |||
.extracting(QualityGate::getId, QualityGate::getIsCaycCompliant) | |||
.extracting(QualityGate::getId, QualityGate::getCaycStatus) | |||
.containsExactlyInAnyOrder( | |||
tuple(qualityGate1.getUuid(), true), | |||
tuple(qualityGate2.getUuid(), false)); | |||
tuple(qualityGate1.getUuid(), COMPLIANT.toString()), | |||
tuple(qualityGate2.getUuid(), NON_COMPLIANT.toString()), | |||
tuple(qualityGate3.getUuid(), OVER_COMPLIANT.toString())); | |||
} | |||
@Test |
@@ -23,6 +23,7 @@ import java.io.IOException; | |||
import java.nio.charset.StandardCharsets; | |||
import org.apache.commons.io.IOUtils; | |||
import org.apache.commons.lang.RandomStringUtils; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.api.measures.CoreMetrics; | |||
@@ -37,7 +38,6 @@ import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.component.SnapshotDto; | |||
import org.sonar.db.metric.MetricDto; | |||
import org.sonar.db.permission.GlobalPermission; | |||
import org.sonar.db.qualitygate.QualityGateDto; | |||
import org.sonar.server.component.TestComponentFinder; | |||
import org.sonar.server.exceptions.BadRequestException; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
@@ -53,6 +53,7 @@ import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.assertj.core.api.Assertions.tuple; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.ArgumentMatchers.eq; | |||
import static org.mockito.Mockito.mock; | |||
@@ -61,6 +62,8 @@ import static org.sonar.db.component.SnapshotTesting.newAnalysis; | |||
import static org.sonar.db.measure.MeasureTesting.newLiveMeasure; | |||
import static org.sonar.db.measure.MeasureTesting.newMeasureDto; | |||
import static org.sonar.db.metric.MetricTesting.newMetricDto; | |||
import static org.sonar.server.qualitygate.QualityGateCaycStatus.COMPLIANT; | |||
import static org.sonar.server.qualitygate.QualityGateCaycStatus.NON_COMPLIANT; | |||
import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_ANALYSIS_ID; | |||
import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_BRANCH; | |||
import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_ID; | |||
@@ -82,6 +85,11 @@ public class ProjectStatusActionTest { | |||
private final WsActionTester ws = new WsActionTester(new ProjectStatusAction(dbClient, TestComponentFinder.from(db), userSession, qualityGateCaycChecker)); | |||
@Before | |||
public void setUp() { | |||
when(qualityGateCaycChecker.checkCaycCompliantFromProject(any(), any())).thenReturn(NON_COMPLIANT); | |||
} | |||
@Test | |||
public void test_definition() { | |||
WebService.Action action = ws.getDef(); | |||
@@ -328,7 +336,7 @@ public class ProjectStatusActionTest { | |||
ComponentDto project = db.components().insertPrivateProject(); | |||
var qg = db.qualityGates().insertBuiltInQualityGate(); | |||
db.qualityGates().setDefaultQualityGate(qg); | |||
when(qualityGateCaycChecker.checkCaycCompliantFromProject(any(DbSession.class), eq(project.uuid()))).thenReturn(true); | |||
when(qualityGateCaycChecker.checkCaycCompliantFromProject(any(DbSession.class), eq(project.uuid()))).thenReturn(COMPLIANT); | |||
SnapshotDto snapshot = dbClient.snapshotDao().insert(dbSession, newAnalysis(project)); | |||
dbSession.commit(); | |||
userSession.addProjectPermission(UserRole.USER, project); | |||
@@ -337,7 +345,7 @@ public class ProjectStatusActionTest { | |||
.setParam(PARAM_ANALYSIS_ID, snapshot.getUuid()) | |||
.executeProtobuf(ProjectStatusResponse.class); | |||
assertThat(result.getProjectStatus().getIsCaycCompliant()).isTrue(); | |||
assertEquals(COMPLIANT.toString(), result.getProjectStatus().getCaycStatus()); | |||
} | |||
@Test |
@@ -21,7 +21,6 @@ package org.sonar.server.qualitygate.ws; | |||
import java.io.IOException; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import javax.annotation.Nullable; | |||
import org.apache.commons.io.IOUtils; | |||
import org.junit.Test; | |||
@@ -31,7 +30,9 @@ import org.sonarqube.ws.Qualitygates.ProjectStatusResponse.ProjectStatus; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.sonar.api.utils.DateUtils.formatDateTime; | |||
import static org.sonar.server.qualitygate.QualityGateCaycStatus.NON_COMPLIANT; | |||
public class QualityGateDetailsFormatterTest { | |||
@@ -49,7 +50,7 @@ public class QualityGateDetailsFormatterTest { | |||
ProjectStatus result = underTest.format(); | |||
assertThat(result.getStatus()).isEqualTo(ProjectStatusResponse.Status.ERROR); | |||
assertThat(result.getIsCaycCompliant()).isFalse(); | |||
assertEquals(NON_COMPLIANT.toString(), result.getCaycStatus()); | |||
// check conditions | |||
assertThat(result.getConditionsCount()).isEqualTo(3); | |||
List<ProjectStatusResponse.Condition> conditions = result.getConditionsList(); | |||
@@ -145,6 +146,6 @@ public class QualityGateDetailsFormatterTest { | |||
} | |||
private static QualityGateDetailsFormatter newQualityGateDetailsFormatter(@Nullable String measureData, @Nullable SnapshotDto snapshotDto) { | |||
return new QualityGateDetailsFormatter(measureData, snapshotDto, false); | |||
return new QualityGateDetailsFormatter(measureData, snapshotDto, NON_COMPLIANT); | |||
} | |||
} |
@@ -19,6 +19,7 @@ | |||
*/ | |||
package org.sonar.server.qualitygate.ws; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.api.server.ws.WebService; | |||
@@ -43,12 +44,14 @@ import static java.lang.String.format; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.assertj.core.api.AssertionsForClassTypes.tuple; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.ArgumentMatchers.eq; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_GATES; | |||
import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES; | |||
import static org.sonar.server.qualitygate.QualityGateCaycStatus.COMPLIANT; | |||
import static org.sonar.test.JsonAssert.assertJson; | |||
import static org.sonarqube.ws.Qualitygates.Actions; | |||
@@ -64,6 +67,11 @@ public class ShowActionTest { | |||
new ShowAction(db.getDbClient(), new QualityGateFinder(db.getDbClient()), | |||
new QualityGatesWsSupport(db.getDbClient(), userSession, TestComponentFinder.from(db)), qualityGateCaycChecker)); | |||
@Before | |||
public void setUp() { | |||
when(qualityGateCaycChecker.checkCaycCompliant(any(), any())).thenReturn(COMPLIANT); | |||
} | |||
@Test | |||
public void show() { | |||
QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); | |||
@@ -103,14 +111,14 @@ public class ShowActionTest { | |||
@Test | |||
public void show_isCaycCompliant() { | |||
QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); | |||
when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate.getUuid()))).thenReturn(true); | |||
when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate.getUuid()))).thenReturn(COMPLIANT); | |||
db.qualityGates().setDefaultQualityGate(qualityGate); | |||
ShowWsResponse response = ws.newRequest() | |||
.setParam("name", qualityGate.getName()) | |||
.executeProtobuf(ShowWsResponse.class); | |||
assertThat(response.getIsCaycCompliant()).isTrue(); | |||
assertEquals(COMPLIANT.toString(), response.getCaycStatus()); | |||
} | |||
@Test |
@@ -1849,8 +1849,8 @@ quality_gates.cayc_missing.banner.description={cayc_link} is the most efficient | |||
quality_gates.cayc_condition.review_update=Review and Fix Quality Gate | |||
quality_gates.cayc.review_update_modal.header=Fix "{0}" to comply with Clean as You Code | |||
quality_gates.cayc.review_update_modal.confirm_text=Fix Quality Gate | |||
quality_gates.cayc.review_update_modal.description1=This quality gate will be updated to comply with {cayc_link}. Existing conditions on duplication and coverage on new code will be preserved. All non-compliant conditions will be deleted, inlcuding conditions on overall code. | |||
quality_gates.cayc.review_update_modal.description2=Please review the new quality gate conditions below. | |||
quality_gates.cayc.review_update_modal.description1=This quality gate will be updated to comply with {cayc_link}. Please review the changes below. | |||
quality_gates.cayc.review_update_modal.description2=All other conditions will be preserved | |||
quality_gates.cayc.new_maintainability_rating.A=Technical debt ratio is less than {0} | |||
quality_gates.cayc.new_maintainability_rating=Technical debt ratio is greater than {1} | |||
quality_gates.cayc.new_reliability_rating.A=No bugs | |||
@@ -1858,6 +1858,8 @@ quality_gates.cayc.new_security_rating.A=No vulnerabilities | |||
quality_gates.cayc.unlock_edit=Unlock editing | |||
quality_gates.cayc.tooltip.message=This quality gate does not comply with Clean as You Code. | |||
quality_gates.cayc.badge.tooltip.learn_more=Learn more: Clean as You Code | |||
quality_gates.cayc_over_compliant.tooltip.message=Additional conditions may have potential drawbacks. | |||
quality_gates.cayc_over_compliant.badge.tooltip.learn_more=Learn more: Potential drawbacks | |||
quality_gates.cayc.banner.title=This quality gate complies with Clean as You Code | |||
quality_gates.cayc.banner.description=This quality gate complies with the {cayc_link} methodology, so that you benefit from the most efficient approach to delivering Clean Code. The quality gate ensures that the {new_code_link} you’re working on has: | |||
quality_gates.cayc.banner.list_item1=No bugs | |||
@@ -1865,7 +1867,13 @@ quality_gates.cayc.banner.list_item2=No vulnerabilities | |||
quality_gates.cayc.banner.list_item3=No security hotspots to review | |||
quality_gates.cayc.banner.list_item4=A limited amount of code smells (i.e. low technical debt ratio) | |||
quality_gates.cayc.banner.list_item5=A controlled level of duplication and coverage | |||
quality_gates.cayc_unfollow.description=Are you reconsidering {cayc_link}? We strongly recommend this methodology to achieve a Clean Code state. However, if you still wish to move out of this approach you may edit this quality gate. | |||
quality_gates.cayc_over_compliant.banner.title=This quality gate may have drawbacks | |||
quality_gates.cayc_over_compliant.banner.description1=This quality gate includes additional conditions that may have drawbacks. | |||
quality_gates.cayc_over_compliant.banner.description2=Learn more: {link} | |||
quality_gates.cayc_over_compliant.banner.link=Potential drawbacks | |||
quality_gates.cayc_unfollow.description=You may click unlock to edit this quality gate. Adding extra conditions to a compliant quality gate can result in drawbacks. Are you reconsidering {cayc_link}? We strongly recommend this methodology to achieve a Clean Code status. | |||
quality_gates.cayc.review_update_modal.add_condition.header= {0} condition(s) on New Code will be added | |||
quality_gates.cayc.review_update_modal.modify_condition.header= {0} condition(s) on New Code will be modified | |||
#------------------------------------------------------------------------------ | |||
# | |||
@@ -3265,9 +3273,15 @@ overview.quality_gate.ignored_conditions.tooltip=At the start of a new code peri | |||
overview.quality_gate.conditions_on_new_code=Only conditions on new code that are defined in the Quality Gate are checked. See the {link} associated to the project for details. | |||
overview.quality_gate.conditions.cayc.warning=The quality gate used by this project does not comply with Clean as You Code. | |||
overview.quality_gate.conditions.cayc.details=Fixing this quality gate will help you achieve a Clean Code state. | |||
overview.quality_gate.conditions.cayc.review=Review Quality Gate | |||
overview.quality_gate.conditions.cayc.details_with_link=Fixing {link} will help you achieve a Clean Code state. | |||
overview.quality_gate.conditions.non_cayc.warning.link=this quality gate | |||
overview.quality_gate.conditions.cayc_over_compliant.warning.link=This quality gate | |||
overview.quality_gate.conditions.cayc_over_compliant.details=This quality gate is Clean as You Code compliant, but it includes additional conditions that may have drawbacks. | |||
overview.quality_gate.conditions.cayc_over_compliant.details_with_link={link} is Clean as You Code compliant, but it includes additional conditions that may have drawbacks. | |||
overview.quality_gate.conditions.cayc.link=Learn more: Clean as You Code | |||
overview.quality_gate.conditions.cayc_over_compliant.link=Learn more: Potential drawbacks | |||
overview.quality_gate.application.non_cayc.projects_x={0} project(s) in this application use a Quality Gate that does not comply with Clean as You Code | |||
overview.quality_gate.application.cayc_over_compliant.projects_x={0} project(s) in this application use a Clean as You Code compliant quality gate with extra conditions. This may result in drawbacks. | |||
overview.quality_gate.show_project_conditions_x=Show failed conditions for project {0} | |||
overview.quality_gate.hide_project_conditions_x=Hide failed conditions for project {0} | |||
overview.quality_profiles=Quality Profiles used |
@@ -36,7 +36,7 @@ message ProjectStatusResponse { | |||
repeated Period periods = 3; | |||
optional bool ignoredConditions = 4; | |||
optional NewCodePeriod period = 5; | |||
optional bool isCaycCompliant = 6; | |||
optional string caycStatus = 6; | |||
} | |||
message Condition { | |||
@@ -131,7 +131,7 @@ message ShowWsResponse { | |||
repeated Condition conditions = 3; | |||
optional bool isBuiltIn = 4; | |||
optional Actions actions = 5; | |||
optional bool isCaycCompliant = 6; | |||
optional string caycStatus = 6; | |||
message Condition { | |||
optional string id = 1; | |||
@@ -168,7 +168,7 @@ message ListWsResponse { | |||
optional bool isDefault = 3; | |||
optional bool isBuiltIn = 4; | |||
optional Actions actions = 5; | |||
optional bool isCaycCompliant = 6; | |||
optional string caycStatus = 6; | |||
} | |||
message RootActions { |