Browse Source

SONAR-17815 implement updated logic for CaYC quality gates

tags/10.0.0.68432
Matteo Mara 1 year ago
parent
commit
2d60913db1
44 changed files with 727 additions and 250 deletions
  1. 1
    1
      server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryData.java
  2. 1
    1
      server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryDataJsonWriter.java
  3. 6
    5
      server/sonar-server-common/src/test/java/org/sonar/server/telemetry/TelemetryDataJsonWriterTest.java
  4. 39
    7
      server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts
  5. 86
    0
      server/sonar-web/src/main/js/apps/overview/branches/ApplicationNonCaycProjectWarning.tsx
  6. 4
    4
      server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
  7. 19
    11
      server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarning.tsx
  8. 62
    0
      server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarningOverCompliant.tsx
  9. 21
    40
      server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx
  10. 17
    8
      server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx
  11. 24
    6
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx
  12. 3
    1
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/QualityGatePanelSection-test.tsx
  13. 6
    6
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanel-test.tsx.snap
  14. 4
    4
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanelSection-test.tsx.snap
  15. 35
    0
      server/sonar-web/src/main/js/apps/quality-gates/components/CaycOverCompliantBadgeTooltip.tsx
  16. 36
    7
      server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx
  17. 50
    30
      server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx
  18. 22
    5
      server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValue.tsx
  19. 46
    19
      server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
  20. 3
    3
      server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx
  21. 10
    7
      server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
  22. 2
    2
      server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx
  23. 25
    1
      server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
  24. 16
    0
      server/sonar-web/src/main/js/apps/quality-gates/styles.css
  25. 20
    18
      server/sonar-web/src/main/js/apps/quality-gates/utils.ts
  26. 5
    5
      server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts
  27. 4
    4
      server/sonar-web/src/main/js/types/quality-gates.ts
  28. 7
    1
      server/sonar-web/src/main/js/types/types.ts
  29. 1
    1
      server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java
  30. 7
    4
      server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDataLoaderImplTest.java
  31. 15
    6
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycChecker.java
  32. 38
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycStatus.java
  33. 2
    2
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ListAction.java
  34. 4
    3
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ProjectStatusAction.java
  35. 6
    5
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java
  36. 6
    5
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ShowAction.java
  37. 1
    1
      server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/project_status-example.json
  38. 9
    6
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateCaycCheckerTest.java
  39. 18
    6
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ListActionTest.java
  40. 11
    3
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ProjectStatusActionTest.java
  41. 4
    3
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest.java
  42. 10
    2
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ShowActionTest.java
  43. 18
    4
      sonar-core/src/main/resources/org/sonar/l10n/core.properties
  44. 3
    3
      sonar-ws/src/main/protobuf/ws-qualitygates.proto

+ 1
- 1
server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryData.java View File

record Project(String projectUuid, Long lastAnalysis, String language, Long loc) { 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 { public static class ProjectStatistics {

+ 1
- 1
server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryDataJsonWriter.java View File

statistics.getQualityGates().forEach(qualityGate -> { statistics.getQualityGates().forEach(qualityGate -> {
json.beginObject(); json.beginObject();
json.prop("uuid", qualityGate.uuid()); json.prop("uuid", qualityGate.uuid());
json.prop("isCaycCompliant", qualityGate.isCaycCompliant());
json.prop("caycStatus", qualityGate.caycStatus());
json.endObject(); json.endObject();
}); });
json.endArray(); json.endArray();

+ 6
- 5
server/sonar-server-common/src/test/java/org/sonar/server/telemetry/TelemetryDataJsonWriterTest.java View File

"quality-gates": [ "quality-gates": [
{ {
"uuid": "uuid-0", "uuid": "uuid-0",
"isCaycCompliant": true
"caycStatus": "non-compliant"
}, },
{ {
"uuid": "uuid-1", "uuid": "uuid-1",
"isCaycCompliant": false
"caycStatus": "compliant"
}, },
{ {
"uuid": "uuid-2", "uuid": "uuid-2",
"isCaycCompliant": true
"caycStatus": "over-compliant"
} }
] ]
} }
} }


private List<TelemetryData.QualityGate> attachQualityGates() { 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 @DataProvider

+ 39
- 7
server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts View File

import { mockUserBase } from '../../helpers/mocks/users'; import { mockUserBase } from '../../helpers/mocks/users';
import { mockCondition, mockGroup } from '../../helpers/testMocks'; import { mockCondition, mockGroup } from '../../helpers/testMocks';
import { MetricKey } from '../../types/metrics'; import { MetricKey } from '../../types/metrics';
import { Condition, QualityGate } from '../../types/types';
import { CaycStatus, Condition, QualityGate } from '../../types/types';
import { import {
addGroup, addGroup,
addUser, addUser,
], ],
isDefault: true, isDefault: true,
isBuiltIn: false, isBuiltIn: false,
isCaycCompliant: true,
caycStatus: CaycStatus.Compliant,
}), }),
mockQualityGate({ mockQualityGate({
name: 'SonarSource way - CFamily', name: 'SonarSource way - CFamily',
], ],
isDefault: false, isDefault: false,
isBuiltIn: false, isBuiltIn: false,
caycStatus: CaycStatus.NonCompliant,
}), }),
mockQualityGate({ mockQualityGate({
name: 'Sonar way', name: 'Sonar way',
], ],
isDefault: false, isDefault: false,
isBuiltIn: true, isBuiltIn: true,
isCaycCompliant: true,
caycStatus: CaycStatus.Compliant,
}), }),
mockQualityGate({ mockQualityGate({
name: 'Non Cayc QG', name: 'Non Cayc QG',
], ],
isDefault: false, isDefault: false,
isBuiltIn: 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({ mockQualityGate({
name: 'QG without conditions', name: 'QG without conditions',
conditions: [], conditions: [],
isDefault: false, isDefault: false,
isBuiltIn: false, isBuiltIn: false,
isCaycCompliant: false,
caycStatus: CaycStatus.NonCompliant,
}), }),
mockQualityGate({ mockQualityGate({
name: 'QG without new code conditions', name: 'QG without new code conditions',
], ],
isDefault: false, isDefault: false,
isBuiltIn: false, isBuiltIn: false,
isCaycCompliant: false,
caycStatus: CaycStatus.NonCompliant,
}), }),
]; ];


], ],
isDefault: false, isDefault: false,
isBuiltIn: false, isBuiltIn: false,
isCaycCompliant: true,
caycStatus: CaycStatus.Compliant,
}) })
); );
return this.reply({ return this.reply({

+ 86
- 0
server/sonar-web/src/main/js/apps/overview/branches/ApplicationNonCaycProjectWarning.tsx View File

/*
* 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>
);
}

+ 4
- 4
server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx View File

if (this.mounted) { if (this.mounted) {
const qgStatuses = results const qgStatuses = results
.map(({ measures = [], project, projectBranchLike }) => { .map(({ measures = [], project, projectBranchLike }) => {
const { key, name, status, isCaycCompliant } = project;
const { key, name, status, caycStatus } = project;
const conditions = extractStatusConditionsFromApplicationStatusChildProject(project); const conditions = extractStatusConditionsFromApplicationStatusChildProject(project);
const failedConditions = this.getFailedConditions(conditions, measures); const failedConditions = this.getFailedConditions(conditions, measures);


return { return {
failedConditions, failedConditions,
isCaycCompliant,
caycStatus,
key, key,
name, name,
status, status,
this.loadMeasuresAndMeta(key, branch, metricKeys).then( this.loadMeasuresAndMeta(key, branch, metricKeys).then(
({ measures, metrics, period }) => { ({ measures, metrics, period }) => {
if (this.mounted && measures) { if (this.mounted && measures) {
const { ignoredConditions, isCaycCompliant, status } = projectStatus;
const { ignoredConditions, caycStatus, status } = projectStatus;
const conditions = extractStatusConditionsFromProjectStatus(projectStatus); const conditions = extractStatusConditionsFromProjectStatus(projectStatus);
const failedConditions = this.getFailedConditions(conditions, measures); const failedConditions = this.getFailedConditions(conditions, measures);


const qgStatus = { const qgStatus = {
ignoredConditions, ignoredConditions,
isCaycCompliant,
caycStatus,
failedConditions, failedConditions,
key, key,
name, name,

+ 19
- 11
server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarning.tsx View File

* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import DocLink from '../../../components/common/DocLink'; import DocLink from '../../../components/common/DocLink';
import Link from '../../../components/common/Link'; import Link from '../../../components/common/Link';
import { Alert } from '../../../components/ui/Alert'; import { Alert } from '../../../components/ui/Alert';
return ( return (
<> <>
<Alert variant="warning">{translate('overview.quality_gate.conditions.cayc.warning')}</Alert> <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/"> <DocLink to="/user-guide/clean-as-you-code/">

+ 62
- 0
server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarningOverCompliant.tsx View File

/*
* 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>
</>
);
}

+ 21
- 40
server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx View File

import classNames from 'classnames'; import classNames from 'classnames';
import { flatMap } from 'lodash'; import { flatMap } from 'lodash';
import * as React from 'react'; import * as React from 'react';
import Link from '../../../components/common/Link';
import HelpTooltip from '../../../components/controls/HelpTooltip'; import HelpTooltip from '../../../components/controls/HelpTooltip';
import QualifierIcon from '../../../components/icons/QualifierIcon';
import { Alert } from '../../../components/ui/Alert'; import { Alert } from '../../../components/ui/Alert';
import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { translate, translateWithParameters } from '../../../helpers/l10n'; import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getProjectQueryUrl } from '../../../helpers/urls';
import { ComponentQualifier, isApplication } from '../../../types/component'; import { ComponentQualifier, isApplication } from '../../../types/component';
import { QualityGateStatus } from '../../../types/quality-gates'; import { QualityGateStatus } from '../../../types/quality-gates';
import { Component } from '../../../types/types';
import { CaycStatus, Component } from '../../../types/types';
import SonarLintPromotion from '../components/SonarLintPromotion'; import SonarLintPromotion from '../components/SonarLintPromotion';
import ApplicationNonCaycProjectWarning from './ApplicationNonCaycProjectWarning';
import QualityGatePanelSection from './QualityGatePanelSection'; import QualityGatePanelSection from './QualityGatePanelSection';


export interface QualityGatePanelProps { export interface QualityGatePanelProps {


const nonCaycProjectsInApp = isApplication(component.qualifier) const nonCaycProjectsInApp = isApplication(component.qualifier)
? qgStatuses ? 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' })) .sort(({ name: a }, { name: b }) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
: []; : [];


</div> </div>


{(overallFailedConditionsCount > 0 || {(overallFailedConditionsCount > 0 ||
qgStatuses.some(({ isCaycCompliant }) => !isCaycCompliant)) && (
qgStatuses.some(({ caycStatus }) => caycStatus !== CaycStatus.Compliant)) && (
<div data-test="overview__quality-gate-conditions"> <div data-test="overview__quality-gate-conditions">
{qgStatuses.map((qgStatus) => ( {qgStatuses.map((qgStatus) => (
<QualityGatePanelSection <QualityGatePanelSection
)} )}


{nonCaycProjectsInApp.length > 0 && ( {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}
/>
)} )}
</> </>
)} )}

+ 17
- 8
server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx View File

QualityGateStatus, QualityGateStatus,
QualityGateStatusConditionEnhanced, QualityGateStatusConditionEnhanced,
} from '../../../types/quality-gates'; } from '../../../types/quality-gates';
import { Component } from '../../../types/types';
import { CaycStatus, Component } from '../../../types/types';
import QualityGateConditions from '../components/QualityGateConditions'; import QualityGateConditions from '../components/QualityGateConditions';
import CleanAsYouCodeWarning from './CleanAsYouCodeWarning'; import CleanAsYouCodeWarning from './CleanAsYouCodeWarning';
import CleanAsYouCodeWarningOverCompliant from './CleanAsYouCodeWarningOverCompliant';


export interface QualityGatePanelSectionProps { export interface QualityGatePanelSectionProps {
branchLike?: BranchLike; branchLike?: BranchLike;
if ( if (
!( !(
qgStatus.failedConditions.length > 0 || qgStatus.failedConditions.length > 0 ||
(!qgStatus.isCaycCompliant && !isApplication(component.qualifier))
(qgStatus.caycStatus !== CaycStatus.Compliant && !isApplication(component.qualifier))
) )
) { ) {
return null; return null;


const showSectionTitles = const showSectionTitles =
isApplication(component.qualifier) || isApplication(component.qualifier) ||
!qgStatus.isCaycCompliant ||
qgStatus.caycStatus !== CaycStatus.Compliant ||
(overallFailedConditions.length > 0 && newCodeFailedConditions.length > 0); (overallFailedConditions.length > 0 && newCodeFailedConditions.length > 0);


const toggleLabel = collapsed const toggleLabel = collapsed


{!collapsed && ( {!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 && ( {newCodeFailedConditions.length > 0 && (
<> <>

+ 24
- 6
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx View File

import { ComponentQualifier } from '../../../../types/component'; import { ComponentQualifier } from '../../../../types/component';
import { MetricKey } from '../../../../types/metrics'; import { MetricKey } from '../../../../types/metrics';
import { GraphType } from '../../../../types/project-activity'; 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'; import BranchOverview, { BRANCH_OVERVIEW_ACTIVITY_GRAPH, NO_CI_DETECTED } from '../BranchOverview';


jest.mock('../../../../api/measures', () => { jest.mock('../../../../api/measures', () => {
jest jest
.mocked(getQualityGateProjectStatus) .mocked(getQualityGateProjectStatus)
.mockResolvedValueOnce( .mockResolvedValueOnce(
mockQualityGateProjectStatus({ status: 'OK', isCaycCompliant: false })
mockQualityGateProjectStatus({ status: 'OK', caycStatus: CaycStatus.NonCompliant })
); );


renderBranchOverview(); renderBranchOverview();
it("should show projects that don't have a compliant quality gate", async () => { it("should show projects that don't have a compliant quality gate", async () => {
const appStatus = mockQualityGateApplicationStatus({ const appStatus = mockQualityGateApplicationStatus({
projects: [ 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', key: '4',
name: 'four', name: 'four',
errorThreshold: '0', errorThreshold: '0',
}, },
], ],
isCaycCompliant: false,
caycStatus: CaycStatus.NonCompliant,
status: 'ERROR', status: 'ERROR',
}, },
], ],

+ 3
- 1
server/sonar-web/src/main/js/apps/overview/branches/__tests__/QualityGatePanelSection-test.tsx View File

} from '../../../../helpers/mocks/quality-gates'; } from '../../../../helpers/mocks/quality-gates';
import { ComponentQualifier } from '../../../../types/component'; import { ComponentQualifier } from '../../../../types/component';
import { MetricKey } from '../../../../types/metrics'; import { MetricKey } from '../../../../types/metrics';
import { CaycStatus } from '../../../../types/types';
import { QualityGatePanelSection, QualityGatePanelSectionProps } from '../QualityGatePanelSection'; import { QualityGatePanelSection, QualityGatePanelSectionProps } from '../QualityGatePanelSection';


it('should render correctly', () => { it('should render correctly', () => {
qgStatus: mockQualityGateStatus({ qgStatus: mockQualityGateStatus({
failedConditions: [], failedConditions: [],
status: 'OK', status: 'OK',
caycStatus: CaycStatus.Compliant,
}), }),
}).type() }).type()
).toBeNull(); ).toBeNull();
mockQualityGateStatusConditionEnhanced({ metric: MetricKey.new_bugs }), mockQualityGateStatusConditionEnhanced({ metric: MetricKey.new_bugs }),
], ],
status: 'ERROR', status: 'ERROR',
isCaycCompliant: false,
caycStatus: CaycStatus.NonCompliant,
})} })}
{...props} {...props}
/> />

+ 6
- 6
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanel-test.tsx.snap View File

key="foo" key="foo"
qgStatus={ qgStatus={
{ {
"caycStatus": "compliant",
"failedConditions": [ "failedConditions": [
{ {
"actual": "10", "actual": "10",
}, },
], ],
"ignoredConditions": false, "ignoredConditions": false,
"isCaycCompliant": true,
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"status": "ERROR", "status": "ERROR",
key="foo" key="foo"
qgStatus={ qgStatus={
{ {
"caycStatus": "compliant",
"failedConditions": [ "failedConditions": [
{ {
"actual": "10", "actual": "10",
}, },
], ],
"ignoredConditions": false, "ignoredConditions": false,
"isCaycCompliant": true,
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"status": "ERROR", "status": "ERROR",
key="foo" key="foo"
qgStatus={ qgStatus={
{ {
"caycStatus": "compliant",
"failedConditions": [ "failedConditions": [
{ {
"actual": "10", "actual": "10",
}, },
], ],
"ignoredConditions": false, "ignoredConditions": false,
"isCaycCompliant": true,
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"status": "ERROR", "status": "ERROR",
key="foo" key="foo"
qgStatus={ qgStatus={
{ {
"caycStatus": "compliant",
"failedConditions": [], "failedConditions": [],
"ignoredConditions": false, "ignoredConditions": false,
"isCaycCompliant": true,
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"status": "OK", "status": "OK",
key="foo" key="foo"
qgStatus={ qgStatus={
{ {
"caycStatus": "compliant",
"failedConditions": [ "failedConditions": [
{ {
"actual": "10", "actual": "10",
}, },
], ],
"ignoredConditions": false, "ignoredConditions": false,
"isCaycCompliant": true,
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"status": "ERROR", "status": "ERROR",
key="foo" key="foo"
qgStatus={ qgStatus={
{ {
"caycStatus": "compliant",
"failedConditions": [ "failedConditions": [
{ {
"actual": "10", "actual": "10",
}, },
], ],
"ignoredConditions": true, "ignoredConditions": true,
"isCaycCompliant": true,
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"status": "ERROR", "status": "ERROR",

+ 4
- 4
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanelSection-test.tsx.snap View File

<Memo(QualityGateConditions) <Memo(QualityGateConditions)
component={ component={
{ {
"caycStatus": "non-compliant",
"failedConditions": [ "failedConditions": [
{ {
"actual": "10", "actual": "10",
}, },
], ],
"ignoredConditions": false, "ignoredConditions": false,
"isCaycCompliant": false,
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"status": "ERROR", "status": "ERROR",
<Memo(QualityGateConditions) <Memo(QualityGateConditions)
component={ component={
{ {
"caycStatus": "non-compliant",
"failedConditions": [ "failedConditions": [
{ {
"actual": "10", "actual": "10",
}, },
], ],
"ignoredConditions": false, "ignoredConditions": false,
"isCaycCompliant": false,
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"status": "ERROR", "status": "ERROR",
<Memo(QualityGateConditions) <Memo(QualityGateConditions)
component={ component={
{ {
"caycStatus": "non-compliant",
"failedConditions": [ "failedConditions": [
{ {
"actual": "10", "actual": "10",
}, },
], ],
"ignoredConditions": false, "ignoredConditions": false,
"isCaycCompliant": false,
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"status": "ERROR", "status": "ERROR",
<Memo(QualityGateConditions) <Memo(QualityGateConditions)
component={ component={
{ {
"caycStatus": "non-compliant",
"failedConditions": [ "failedConditions": [
{ {
"actual": "10", "actual": "10",
}, },
], ],
"ignoredConditions": false, "ignoredConditions": false,
"isCaycCompliant": false,
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"status": "ERROR", "status": "ERROR",

+ 35
- 0
server/sonar-web/src/main/js/apps/quality-gates/components/CaycOverCompliantBadgeTooltip.tsx View File

/*
* 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>
);
}

+ 36
- 7
server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx View File

import * as React from 'react'; import * as React from 'react';
import { deleteCondition } from '../../../api/quality-gates'; import { deleteCondition } from '../../../api/quality-gates';
import withMetricsContext from '../../../app/components/metrics/withMetricsContext'; import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
import { colors } from '../../../app/theme';
import { DeleteButton, EditButton } from '../../../components/controls/buttons'; import { DeleteButton, EditButton } from '../../../components/controls/buttons';
import ConfirmModal from '../../../components/controls/ConfirmModal'; 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 { 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 ConditionModal from './ConditionModal';
import ConditionValue from './ConditionValue'; import ConditionValue from './ConditionValue';


modal: boolean; modal: boolean;
} }


const TOOLTIP_MOUSE_LEAVE_DELAY = 0.3;
export class ConditionComponent extends React.PureComponent<Props, State> { export class ConditionComponent extends React.PureComponent<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
isCaycModal = false, isCaycModal = false,
} = this.props; } = this.props;


const isCaycCompliantAndOverCompliant = qualityGate.caycStatus !== CaycStatus.NonCompliant;

return ( return (
<tr className={classNames({ highlighted: updated })}> <tr className={classNames({ highlighted: updated })}>
<td className="text-middle"> <td className="text-middle">
metric={metric} metric={metric}
isCaycModal={isCaycModal} isCaycModal={isCaycModal}
condition={condition} condition={condition}
isCaycCompliant={qualityGate.isCaycCompliant}
isCaycCompliantAndOverCompliant={isCaycCompliantAndOverCompliant}
/> />
</td> </td>
<td className="text-middle nowrap display-flex-justify-end"> <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 && ( {!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 <EditButton
aria-label={translateWithParameters( aria-label={translateWithParameters(
)} )}
</> </>
)} )}
{(!qualityGate.isCaycCompliant || (qualityGate.isCaycCompliant && showEdit)) && (
{(!isCaycCompliantAndOverCompliant ||
!isCaycCondition(condition) ||
(isCaycCompliantAndOverCompliant && showEdit)) && (
<> <>
<DeleteButton <DeleteButton
aria-label={translateWithParameters( aria-label={translateWithParameters(

+ 50
- 30
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx View File

import { sortBy } from 'lodash'; import { sortBy } from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; 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 DocLink from '../../../components/common/DocLink';
import ConfirmModal from '../../../components/controls/ConfirmModal'; import ConfirmModal from '../../../components/controls/ConfirmModal';
import { translate, translateWithParameters } from '../../../helpers/l10n'; import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Condition, Dict, Metric, QualityGate } from '../../../types/types'; import { Condition, Dict, Metric, QualityGate } from '../../../types/types';
import {
getCaycConditionsWithCorrectValue,
getCorrectCaycCondition,
getWeakMissingAndNonCaycConditions,
} from '../utils';
import { getCorrectCaycCondition, getWeakMissingAndNonCaycConditions } from '../utils';
import ConditionsTable from './ConditionsTable'; import ConditionsTable from './ConditionsTable';


interface Props { interface Props {
updateCaycQualityGate = () => { updateCaycQualityGate = () => {
const { conditions, qualityGate } = this.props; const { conditions, qualityGate } = this.props;
const promiseArr: Promise<Condition | undefined | void>[] = []; const promiseArr: Promise<Condition | undefined | void>[] = [];
const { weakConditions, missingConditions, nonCaycConditions } =
getWeakMissingAndNonCaycConditions(conditions);
const { weakConditions, missingConditions } = getWeakMissingAndNonCaycConditions(conditions);


weakConditions.forEach((condition) => { weakConditions.forEach((condition) => {
promiseArr.push( promiseArr.push(
); );
}); });


nonCaycConditions.forEach((condition) => {
promiseArr.push(
deleteCondition({ id: condition.id })
.then(() => this.props.onRemoveCondition(condition))
.catch(() => undefined)
);
});

return Promise.all(promiseArr).then(() => { return Promise.all(promiseArr).then(() => {
this.props.lockEditing(); this.props.lockEditing();
}); });


render() { render() {
const { conditions, qualityGate, metrics } = this.props; 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 (condition) => metrics[condition.metric] && metrics[condition.metric].name
); );

return ( return (
<ConfirmModal <ConfirmModal
header={translateWithParameters( header={translateWithParameters(
}} }}
/> />
</p> </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')} {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> </div>
</ConfirmModal> </ConfirmModal>
); );

+ 22
- 5
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValue.tsx View File

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import classNames from 'classnames';
import * as React from 'react'; import * as React from 'react';
import { formatMeasure } from '../../../helpers/measures'; import { formatMeasure } from '../../../helpers/measures';
import { Condition, Metric } from '../../../types/types'; import { Condition, Metric } from '../../../types/types';
condition: Condition; condition: Condition;
isCaycModal?: boolean; isCaycModal?: boolean;
metric: Metric; metric: Metric;
isCaycCompliant?: boolean;
isCaycCompliantAndOverCompliant?: boolean;
} }


function ConditionValue({ condition, isCaycModal, metric, isCaycCompliant }: Props) {
function ConditionValue({
condition,
isCaycModal,
metric,
isCaycCompliantAndOverCompliant,
}: Props) {
if (isCaycModal) { if (isCaycModal) {
const isToBeModified = condition.error !== getCorrectCaycCondition(condition).error;

return ( 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)} {formatMeasure(getCorrectCaycCondition(condition).error, metric.type)}
</span> </span>
<ConditionValueDescription condition={getCorrectCaycCondition(condition)} metric={metric} />
<ConditionValueDescription
className={classNames({ 'green-text': isToBeModified })}
condition={getCorrectCaycCondition(condition)}
metric={metric}
/>
</> </>
); );
} }
return ( return (
<> <>
<span className="spacer-right">{formatMeasure(condition.error, metric.type)}</span> <span className="spacer-right">{formatMeasure(condition.error, metric.type)}</span>
{isCaycCompliant && isCaycCondition(condition) && (
{isCaycCompliantAndOverCompliant && isCaycCondition(condition) && (
<ConditionValueDescription condition={condition} metric={metric} /> <ConditionValueDescription condition={condition} metric={metric} />
)} )}
</> </>

+ 46
- 19
server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx View File

import { isDiffMetric } from '../../../helpers/measures'; import { isDiffMetric } from '../../../helpers/measures';
import { Feature } from '../../../types/features'; import { Feature } from '../../../types/features';
import { MetricKey } from '../../../types/metrics'; 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 ConditionModal from './ConditionModal';
import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal'; import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal';
import ConditionsTable from './ConditionsTable'; import ConditionsTable from './ConditionsTable';
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
unlockEditing: !props.qualityGate.isCaycCompliant,
unlockEditing: props.qualityGate.caycStatus === CaycStatus.NonCompliant,
}; };
} }


componentDidUpdate(prevProps: Readonly<Props>): void { componentDidUpdate(prevProps: Readonly<Props>): void {
const { qualityGate } = this.props; const { qualityGate } = this.props;
if (prevProps.qualityGate.name !== qualityGate.name) { if (prevProps.qualityGate.name !== qualityGate.name) {
this.setState({ unlockEditing: !qualityGate.isCaycCompliant });
this.setState({ unlockEditing: qualityGate.caycStatus === CaycStatus.NonCompliant });
} }
} }




return ( return (
<div className="quality-gate-section"> <div className="quality-gate-section">
{qualityGate.isCaycCompliant && (
{qualityGate.caycStatus !== CaycStatus.NonCompliant && (
<Alert className="big-spacer-top big-spacer-bottom cayc-success-banner" variant="success"> <Alert className="big-spacer-top big-spacer-bottom cayc-success-banner" variant="success">
<h4 className="spacer-bottom cayc-success-header"> <h4 className="spacer-bottom cayc-success-header">
{translate('quality_gates.cayc.banner.title')} {translate('quality_gates.cayc.banner.title')}
</DocLink> </DocLink>
), ),
new_code_link: ( new_code_link: (
<DocLink to="/project-administration/defining-new-code//">
<DocLink to="/project-administration/defining-new-code/">
{translate('quality_gates.cayc.new_code')} {translate('quality_gates.cayc.new_code')}
</DocLink> </DocLink>
), ),
</Alert> </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"> <Alert className="big-spacer-top big-spacer-bottom" variant="warning">
<h4 className="spacer-bottom cayc-warning-header"> <h4 className="spacer-bottom cayc-warning-header">
{translate('quality_gates.cayc_missing.banner.title')} {translate('quality_gates.cayc_missing.banner.title')}
</Alert> </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"> <header className="display-flex-center">
<h2 className="big">{translate('quality_gates.conditions')}</h2> <h2 className="big">{translate('quality_gates.conditions')}</h2>
</div> </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"> <div className="big-spacer-top big-spacer-bottom cayc-warning-description it__qg-unfollow-cayc">
<p> <p>
<FormattedMessage <FormattedMessage

+ 3
- 3
server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx View File

addGlobalSuccessMessage(translate('quality_gates.condition_added')); addGlobalSuccessMessage(translate('quality_gates.condition_added'));


const updatedQualityGate = addCondition(clone(qualityGate), condition); const updatedQualityGate = addCondition(clone(qualityGate), condition);
if (qualityGate.isCaycCompliant !== updatedQualityGate.isCaycCompliant) {
if (qualityGate.caycStatus !== updatedQualityGate.caycStatus) {
this.props.refreshQualityGates(); this.props.refreshQualityGates();
} }


} }
addGlobalSuccessMessage(translate('quality_gates.condition_updated')); addGlobalSuccessMessage(translate('quality_gates.condition_updated'));
const updatedQualityGate = replaceCondition(clone(qualityGate), newCondition, oldCondition); const updatedQualityGate = replaceCondition(clone(qualityGate), newCondition, oldCondition);
if (qualityGate.isCaycCompliant !== updatedQualityGate.isCaycCompliant) {
if (qualityGate.caycStatus !== updatedQualityGate.caycStatus) {
this.props.refreshQualityGates(); this.props.refreshQualityGates();
} }
return { return {
} }
addGlobalSuccessMessage(translate('quality_gates.condition_deleted')); addGlobalSuccessMessage(translate('quality_gates.condition_deleted'));
const updatedQualityGate = deleteCondition(clone(qualityGate), condition); const updatedQualityGate = deleteCondition(clone(qualityGate), condition);
if (qualityGate.isCaycCompliant !== updatedQualityGate.isCaycCompliant) {
if (qualityGate.caycStatus !== updatedQualityGate.caycStatus) {
this.props.refreshQualityGates(); this.props.refreshQualityGates();
} }
return { return {

+ 10
- 7
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx View File

import Tooltip from '../../../components/controls/Tooltip'; import Tooltip from '../../../components/controls/Tooltip';
import AlertWarnIcon from '../../../components/icons/AlertWarnIcon'; import AlertWarnIcon from '../../../components/icons/AlertWarnIcon';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { QualityGate } from '../../../types/types';
import { CaycStatus, QualityGate } from '../../../types/types';
import BuiltInQualityGateBadge from './BuiltInQualityGateBadge'; import BuiltInQualityGateBadge from './BuiltInQualityGateBadge';
import CaycBadgeTooltip from './CaycBadgeTooltip'; import CaycBadgeTooltip from './CaycBadgeTooltip';
import CopyQualityGateForm from './CopyQualityGateForm'; import CopyQualityGateForm from './CopyQualityGateForm';
render() { render() {
const { qualityGate } = this.props; const { qualityGate } = this.props;
const actions = qualityGate.actions || ({} as any); const actions = qualityGate.actions || ({} as any);
const { isCaycCompliant } = qualityGate;


return ( return (
<div className="layout-page-header-panel layout-page-main-header issues-main-header"> <div className="layout-page-header-panel layout-page-main-header issues-main-header">
<div className="pull-left display-flex-center"> <div className="pull-left display-flex-center">
<h2>{qualityGate.name}</h2> <h2>{qualityGate.name}</h2>
{qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="spacer-left" />} {qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="spacer-left" />}
{!qualityGate.isCaycCompliant && (
{qualityGate.caycStatus === CaycStatus.NonCompliant && (
<Tooltip overlay={<CaycBadgeTooltip />} mouseLeaveDelay={TOOLTIP_MOUSE_LEAVE_DELAY}> <Tooltip overlay={<CaycBadgeTooltip />} mouseLeaveDelay={TOOLTIP_MOUSE_LEAVE_DELAY}>
<AlertWarnIcon className="spacer-left" /> <AlertWarnIcon className="spacer-left" />
</Tooltip> </Tooltip>
{({ onClick }) => ( {({ onClick }) => (
<Tooltip <Tooltip
overlay={ overlay={
!isCaycCompliant ? translate('quality_gates.cannot_copy_no_cayc') : null
qualityGate.caycStatus === CaycStatus.NonCompliant
? translate('quality_gates.cannot_copy_no_cayc')
: null
} }
accessible={false} accessible={false}
> >
className="little-spacer-left" className="little-spacer-left"
id="quality-gate-copy" id="quality-gate-copy"
onClick={onClick} onClick={onClick}
disabled={!isCaycCompliant}
disabled={qualityGate.caycStatus === CaycStatus.NonCompliant}
> >
{translate('copy')} {translate('copy')}
</Button> </Button>
{actions.setAsDefault && ( {actions.setAsDefault && (
<Tooltip <Tooltip
overlay={ 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} accessible={false}
> >
<Button <Button
className="little-spacer-left" className="little-spacer-left"
disabled={!isCaycCompliant}
disabled={qualityGate.caycStatus === CaycStatus.NonCompliant}
id="quality-gate-toggle-default" id="quality-gate-toggle-default"
onClick={this.handleSetAsDefaultClick} onClick={this.handleSetAsDefaultClick}
> >

+ 2
- 2
server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx View File

import AlertWarnIcon from '../../../components/icons/AlertWarnIcon'; import AlertWarnIcon from '../../../components/icons/AlertWarnIcon';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { getQualityGateUrl } from '../../../helpers/urls'; import { getQualityGateUrl } from '../../../helpers/urls';
import { QualityGate } from '../../../types/types';
import { CaycStatus, QualityGate } from '../../../types/types';
import BuiltInQualityGateBadge from './BuiltInQualityGateBadge'; import BuiltInQualityGateBadge from './BuiltInQualityGateBadge';


interface Props { interface Props {
)} )}
{qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="little-spacer-left" />} {qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="little-spacer-left" />}


{!qualityGate.isCaycCompliant && (
{qualityGate.caycStatus === CaycStatus.NonCompliant && (
<> <>
{/* Adding a11y-hidden span for accessibility */} {/* Adding a11y-hidden span for accessibility */}
<span className="a11y-hidden">{translate('quality_gates.cayc.tooltip.message')}</span> <span className="a11y-hidden">{translate('quality_gates.cayc.tooltip.message')}</span>

+ 25
- 1
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx View File

expect(conditionsWrapper.getAllByText('Coverage')).toHaveLength(2); // This quality gate has duplicate condition expect(conditionsWrapper.getAllByText('Coverage')).toHaveLength(2); // This quality gate has duplicate condition
expect(conditionsWrapper.getByText('Duplicated Lines (%)')).toBeInTheDocument(); 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 () => { it('should show success banner when quality gate is CAYC compliant', async () => {
expect(await conditionsWrapper.findByText('Duplicated Lines (%)')).toBeInTheDocument(); 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 () => { it('should unlock editing option for CAYC conditions', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
handler.setIsAdmin(true); handler.setIsAdmin(true);

+ 16
- 0
server/sonar-web/src/main/js/apps/quality-gates/styles.css View File

color: var(--alertTextSuccess); color: var(--alertTextSuccess);
} }


.cayc-over-compliant-header {
color: var(--veryDarkBlue);
}

.cayc-warning-description { .cayc-warning-description {
line-height: 18px; line-height: 18px;
} }

.red-text {
color: var(--red);
}

.green-text {
color: var(--success500);
}

.strike-through {
text-decoration: line-through;
}

+ 20
- 18
server/sonar-web/src/main/js/apps/quality-gates/utils.ts View File

*/ */
import { getLocalizedMetricName } from '../../helpers/l10n'; import { getLocalizedMetricName } from '../../helpers/l10n';
import { isDiffMetric } from '../../helpers/measures'; 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 } = { const CAYC_CONDITIONS: { [key: string]: Condition } = {
new_reliability_rating: { new_reliability_rating: {
}; };


export const CAYC_CONDITIONS_WITHOUT_FIXED_VALUE = ['new_duplicated_lines_density', 'new_coverage']; 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) { export function isCaycCondition(condition: Condition) {
return condition.metric in CAYC_CONDITIONS; return condition.metric in CAYC_CONDITIONS;
const result: { const result: {
weakConditions: Condition[]; weakConditions: Condition[];
missingConditions: Condition[]; missingConditions: Condition[];
nonCaycConditions: Condition[];
} = { } = {
weakConditions: [], weakConditions: [],
missingConditions: [], missingConditions: [],
nonCaycConditions: [],
}; };
Object.keys(CAYC_CONDITIONS).forEach((key) => { Object.keys(CAYC_CONDITIONS).forEach((key) => {
const selectedCondition = conditions.find((condition) => condition.metric === key); const selectedCondition = conditions.find((condition) => condition.metric === key);
result.weakConditions.push(selectedCondition); result.weakConditions.push(selectedCondition);
} }
}); });

result.nonCaycConditions = getNonCaycConditions(conditions);
return result; return result;
} }


}); });
} }


export function getNonCaycConditions(conditions: Condition[]) {
return conditions.filter((condition) => !isCaycCondition(condition));
}

export function getCorrectCaycCondition(condition: Condition) { export function getCorrectCaycCondition(condition: Condition) {
if (CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(condition.metric)) { if (CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(condition.metric)) {
return condition; return condition;
const oldConditions = qualityGate.conditions || []; const oldConditions = qualityGate.conditions || [];
const conditions = [...oldConditions, condition]; const conditions = [...oldConditions, condition];
if (conditions) { if (conditions) {
qualityGate.isCaycCompliant = updateCaycComplaintStatus(conditions);
qualityGate.caycStatus = updateCaycCompliantStatus(conditions);
} }
return { ...qualityGate, conditions }; return { ...qualityGate, conditions };
} }
const conditions = const conditions =
qualityGate.conditions && qualityGate.conditions.filter((candidate) => candidate !== condition); qualityGate.conditions && qualityGate.conditions.filter((candidate) => candidate !== condition);
if (conditions) { if (conditions) {
qualityGate.isCaycCompliant = updateCaycComplaintStatus(conditions);
qualityGate.caycStatus = updateCaycCompliantStatus(conditions);
} }
return { ...qualityGate, conditions }; return { ...qualityGate, conditions };
} }
return candidate === oldCondition ? newCondition : candidate; return candidate === oldCondition ? newCondition : candidate;
}); });
if (conditions) { if (conditions) {
qualityGate.isCaycCompliant = updateCaycComplaintStatus(conditions);
qualityGate.caycStatus = updateCaycCompliantStatus(conditions);
} }


return { ...qualityGate, 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)) { for (const key of Object.keys(CAYC_CONDITIONS)) {
const selectedCondition = conditions.find((condition) => condition.metric === key); const selectedCondition = conditions.find((condition) => condition.metric === key);
if (!selectedCondition) { if (!selectedCondition) {
return false;
return CaycStatus.NonCompliant;
} }


if ( if (
selectedCondition && selectedCondition &&
selectedCondition.error !== CAYC_CONDITIONS[key].error 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) { export function getPossibleOperators(metric: Metric) {

+ 5
- 5
server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts View File

QualityGateStatusCondition, QualityGateStatusCondition,
QualityGateStatusConditionEnhanced, QualityGateStatusConditionEnhanced,
} from '../../types/quality-gates'; } from '../../types/quality-gates';
import { QualityGate } from '../../types/types';
import { CaycStatus, QualityGate } from '../../types/types';
import { mockMeasureEnhanced, mockMetric } from '../testMocks'; import { mockMeasureEnhanced, mockMetric } from '../testMocks';


export function mockQualityGate(overrides: Partial<QualityGate> = {}): QualityGate { export function mockQualityGate(overrides: Partial<QualityGate> = {}): QualityGate {
): QualityGateStatus { ): QualityGateStatus {
return { return {
ignoredConditions: false, ignoredConditions: false,
isCaycCompliant: true,
caycStatus: CaycStatus.Compliant,
failedConditions: [mockQualityGateStatusConditionEnhanced()], failedConditions: [mockQualityGateStatusConditionEnhanced()],
key: 'foo', key: 'foo',
name: 'Foo', name: 'Foo',
}, },
], ],
ignoredConditions: false, ignoredConditions: false,
isCaycCompliant: true,
caycStatus: CaycStatus.Compliant,
status: 'OK', status: 'OK',
...overrides, ...overrides,
}; };
value: '5', value: '5',
}, },
], ],
isCaycCompliant: true,
caycStatus: CaycStatus.Compliant,
status: 'ERROR', status: 'ERROR',
}, },
{ {
value: '15', value: '15',
}, },
], ],
isCaycCompliant: true,
caycStatus: CaycStatus.Compliant,
status: 'ERROR', status: 'ERROR',
}, },
], ],

+ 4
- 4
server/sonar-web/src/main/js/types/quality-gates.ts View File

* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import { BranchLike } from './branch-like'; import { BranchLike } from './branch-like';
import { MeasureEnhanced, Metric, Status } from './types';
import { CaycStatus, MeasureEnhanced, Metric, Status } from './types';
import { UserBase } from './users'; import { UserBase } from './users';


export interface QualityGateProjectStatus { export interface QualityGateProjectStatus {
conditions?: QualityGateProjectStatusCondition[]; conditions?: QualityGateProjectStatusCondition[];
ignoredConditions: boolean; ignoredConditions: boolean;
status: Status; status: Status;
isCaycCompliant: boolean;
caycStatus: CaycStatus;
} }


export interface QualityGateProjectStatusCondition { export interface QualityGateProjectStatusCondition {
key: string; key: string;
name: string; name: string;
status: Status; status: Status;
isCaycCompliant: boolean;
caycStatus: CaycStatus;
} }


export interface QualityGateStatus { export interface QualityGateStatus {
failedConditions: QualityGateStatusConditionEnhanced[]; failedConditions: QualityGateStatusConditionEnhanced[];
ignoredConditions?: boolean; ignoredConditions?: boolean;
isCaycCompliant: boolean;
caycStatus: CaycStatus;
key: string; key: string;
name: string; name: string;
status: Status; status: Status;

+ 7
- 1
server/sonar-web/src/main/js/types/types.ts View File

url: string; url: string;
} }


export enum CaycStatus {
Compliant = 'compliant',
NonCompliant = 'non-compliant',
OverCompliant = 'over-compliant',
}

export interface QualityGate { export interface QualityGate {
actions?: { actions?: {
associateProjects?: boolean; associateProjects?: boolean;
}; };
conditions?: Condition[]; conditions?: Condition[];
isBuiltIn?: boolean; isBuiltIn?: boolean;
isCaycCompliant?: boolean;
caycStatus?: CaycStatus;
isDefault?: boolean; isDefault?: boolean;
name: string; name: string;
} }

+ 1
- 1
server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java View File

for (QualityGateDto qualityGateDto : qualityGateDtos) { for (QualityGateDto qualityGateDto : qualityGateDtos) {
qualityGates.add( qualityGates.add(
new TelemetryData.QualityGate(qualityGateDto.getUuid(), qualityGateCaycChecker.checkCaycCompliant(dbSession, new TelemetryData.QualityGate(qualityGateDto.getUuid(), qualityGateCaycChecker.checkCaycCompliant(dbSession,
qualityGateDto.getUuid()))
qualityGateDto.getUuid()).toString())
); );
} }



+ 7
- 4
server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDataLoaderImplTest.java View File

import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.Assertions.entry;
import static org.assertj.core.groups.Tuple.tuple; 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.mock;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.sonar.db.component.BranchType.BRANCH; 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_CPP_KEY;
import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_C_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; import static org.sonar.server.telemetry.TelemetryDataLoaderImpl.SCIM_PROPERTY_ENABLED;


@RunWith(DataProviderRunner.class) @RunWith(DataProviderRunner.class)
public void setUpBuiltInQualityGate() { public void setUpBuiltInQualityGate() {
String builtInQgName = "Sonar Way"; String builtInQgName = "Sonar Way";
builtInDefaultQualityGate = db.qualityGates().insertQualityGate(qg -> qg.setName(builtInQgName).setBuiltIn(true)); builtInDefaultQualityGate = db.qualityGates().insertQualityGate(qg -> qg.setName(builtInQgName).setBuiltIn(true));
when(qualityGateCaycChecker.checkCaycCompliant(any(), any())).thenReturn(NON_COMPLIANT);
db.qualityGates().setDefaultQualityGate(builtInDefaultQualityGate); db.qualityGates().setDefaultQualityGate(builtInDefaultQualityGate);


bugsDto = db.measures().insertMetric(m -> m.setKey(BUGS_KEY)); bugsDto = db.measures().insertMetric(m -> m.setKey(BUGS_KEY));
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, 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())); tuple(1L, 0L, builtInDefaultQualityGate.getUuid(), "scm-2", "ci-2", "github_cloud", Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()));
assertThat(data.getQualityGates()) assertThat(data.getQualityGates())
.extracting(TelemetryData.QualityGate::uuid, TelemetryData.QualityGate::isCaycCompliant)
.extracting(TelemetryData.QualityGate::uuid, TelemetryData.QualityGate::caycStatus)
.containsExactlyInAnyOrder( .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")
); );
} }



+ 15
- 6
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycChecker.java View File

import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED; 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.api.measures.CoreMetrics.NEW_SECURITY_RATING;
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; 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 { public class QualityGateCaycChecker {


this.dbClient = dbClient; this.dbClient = dbClient;
} }


public boolean checkCaycCompliant(DbSession dbSession, String qualityGateUuid) {
public QualityGateCaycStatus checkCaycCompliant(DbSession dbSession, String qualityGateUuid) {
var conditionsByMetricId = dbClient.gateConditionDao().selectForQualityGate(dbSession, qualityGateUuid) var conditionsByMetricId = dbClient.gateConditionDao().selectForQualityGate(dbSession, qualityGateUuid)
.stream() .stream()
.collect(uniqueIndex(QualityGateConditionDto::getMetricUuid)); .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()) var metrics = dbClient.metricDao().selectByUuids(dbSession, conditionsByMetricId.keySet())
.filter(metric -> checkMetricCaycCompliant(conditionsByMetricId.get(metric.getUuid()), metric)) .filter(metric -> checkMetricCaycCompliant(conditionsByMetricId.get(metric.getUuid()), metric))
.count(); .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)) return Optional.ofNullable(dbClient.qualityGateDao().selectByProjectUuid(dbSession, projectUuid))
.or(() -> Optional.ofNullable(dbClient.qualityGateDao().selectDefault(dbSession))) .or(() -> Optional.ofNullable(dbClient.qualityGateDao().selectDefault(dbSession)))
.map(qualityGate -> checkCaycCompliant(dbSession, qualityGate.getUuid())) .map(qualityGate -> checkCaycCompliant(dbSession, qualityGate.getUuid()))
.orElse(false);
.orElse(NON_COMPLIANT);
} }


private static boolean checkMetricCaycCompliant(QualityGateConditionDto condition, MetricDto metric) { private static boolean checkMetricCaycCompliant(QualityGateConditionDto condition, MetricDto metric) {

+ 38
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycStatus.java View File

/*
* 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;
}
}

+ 2
- 2
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ListAction.java View File

.setSince("4.3") .setSince("4.3")
.setResponseExample(Resources.getResource(this.getClass(), "list-example.json")) .setResponseExample(Resources.getResource(this.getClass(), "list-example.json"))
.setChangelog( .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("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", "'isDefault' field is added on quality gate"),
new Change("7.0", "'default' field on root level is deprecated"), new Change("7.0", "'default' field on root level is deprecated"),
.setName(qualityGate.getName()) .setName(qualityGate.getName())
.setIsDefault(qualityGate.getUuid().equals(defaultUuid)) .setIsDefault(qualityGate.getUuid().equals(defaultUuid))
.setIsBuiltIn(qualityGate.isBuiltIn()) .setIsBuiltIn(qualityGate.isBuiltIn())
.setIsCaycCompliant(qualityGateCaycChecker.checkCaycCompliant(dbSession, qualityGate.getUuid()))
.setCaycStatus(qualityGateCaycChecker.checkCaycCompliant(dbSession, qualityGate.getUuid()).toString())
.setActions(wsSupport.getActions(dbSession, qualityGate, defaultQualityGate)) .setActions(wsSupport.getActions(dbSession, qualityGate, defaultQualityGate))
.build()) .build())
.collect(toList())); .collect(toList()));

+ 4
- 3
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ProjectStatusAction.java View File

import org.sonar.server.component.ComponentFinder; import org.sonar.server.component.ComponentFinder;
import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.qualitygate.QualityGateCaycChecker; import org.sonar.server.qualitygate.QualityGateCaycChecker;
import org.sonar.server.qualitygate.QualityGateCaycStatus;
import org.sonar.server.user.UserSession; import org.sonar.server.user.UserSession;
import org.sonar.server.ws.KeyExamples; import org.sonar.server.ws.KeyExamples;
import org.sonarqube.ws.Qualitygates.ProjectStatusResponse; import org.sonarqube.ws.Qualitygates.ProjectStatusResponse;
.setSince("5.3") .setSince("5.3")
.setHandler(this) .setHandler(this)
.setChangelog( .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("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("8.5", "The field 'periods' in the response is deprecated. Use 'period' instead"),
new Change("7.7", "The parameters 'branch' and 'pullRequest' were added"), new Change("7.7", "The parameters 'branch' and 'pullRequest' were added"),
ProjectAndSnapshot projectAndSnapshot = getProjectAndSnapshot(dbSession, analysisId, projectUuid, projectKey, branchKey, pullRequestId); ProjectAndSnapshot projectAndSnapshot = getProjectAndSnapshot(dbSession, analysisId, projectUuid, projectKey, branchKey, pullRequestId);
checkPermission(projectAndSnapshot.project); checkPermission(projectAndSnapshot.project);
Optional<String> measureData = loadQualityGateDetails(dbSession, projectAndSnapshot, analysisId != null); 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() 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(); .build();
} }



+ 6
- 5
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java View File

import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.sonar.db.component.SnapshotDto; import org.sonar.db.component.SnapshotDto;
import org.sonar.server.qualitygate.QualityGateCaycStatus;
import org.sonarqube.ws.Qualitygates.ProjectStatusResponse; import org.sonarqube.ws.Qualitygates.ProjectStatusResponse;
import org.sonarqube.ws.Qualitygates.ProjectStatusResponse.NewCodePeriod; import org.sonarqube.ws.Qualitygates.ProjectStatusResponse.NewCodePeriod;
import org.sonarqube.ws.Qualitygates.ProjectStatusResponse.Period; import org.sonarqube.ws.Qualitygates.ProjectStatusResponse.Period;
public class QualityGateDetailsFormatter { public class QualityGateDetailsFormatter {
private final Optional<String> optionalMeasureData; private final Optional<String> optionalMeasureData;
private final Optional<SnapshotDto> optionalSnapshot; private final Optional<SnapshotDto> optionalSnapshot;
private final boolean isCaycCompliant;
private final QualityGateCaycStatus caycStatus;
private final ProjectStatusResponse.ProjectStatus.Builder projectStatusBuilder; 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.optionalMeasureData = Optional.ofNullable(measureData);
this.optionalSnapshot = Optional.ofNullable(snapshot); this.optionalSnapshot = Optional.ofNullable(snapshot);
this.isCaycCompliant = isCaycCompliant;
this.caycStatus = caycStatus;
this.projectStatusBuilder = ProjectStatusResponse.ProjectStatus.newBuilder(); this.projectStatusBuilder = ProjectStatusResponse.ProjectStatus.newBuilder();
} }




ProjectStatusResponse.Status qualityGateStatus = measureLevelToQualityGateStatus(json.get("level").getAsString()); ProjectStatusResponse.Status qualityGateStatus = measureLevelToQualityGateStatus(json.get("level").getAsString());
projectStatusBuilder.setStatus(qualityGateStatus); projectStatusBuilder.setStatus(qualityGateStatus);
projectStatusBuilder.setIsCaycCompliant(isCaycCompliant);
projectStatusBuilder.setCaycStatus(caycStatus.toString());


formatIgnoredConditions(json); formatIgnoredConditions(json);
formatConditions(json.getAsJsonArray("conditions")); formatConditions(json.getAsJsonArray("conditions"));
} }


private ProjectStatusResponse.ProjectStatus newResponseWithoutQualityGateDetails() { 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() { private static Predicate<JsonObject> isConditionOnValidPeriod() {

+ 6
- 5
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ShowAction.java View File

import org.sonar.db.qualitygate.QualityGateConditionDto; import org.sonar.db.qualitygate.QualityGateConditionDto;
import org.sonar.db.qualitygate.QualityGateDto; import org.sonar.db.qualitygate.QualityGateDto;
import org.sonar.server.qualitygate.QualityGateCaycChecker; import org.sonar.server.qualitygate.QualityGateCaycChecker;
import org.sonar.server.qualitygate.QualityGateCaycStatus;
import org.sonar.server.qualitygate.QualityGateFinder; import org.sonar.server.qualitygate.QualityGateFinder;
import org.sonarqube.ws.Qualitygates.ShowWsResponse; import org.sonarqube.ws.Qualitygates.ShowWsResponse;


.setSince("4.3") .setSince("4.3")
.setResponseExample(Resources.getResource(this.getClass(), "show-example.json")) .setResponseExample(Resources.getResource(this.getClass(), "show-example.json"))
.setChangelog( .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", "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("8.4", "Field 'id' in the response is deprecated."),
new Change("7.6", "'period' and 'warning' fields of conditions are removed from the response"), new Change("7.6", "'period' and 'warning' fields of conditions are removed from the response"),
Collection<QualityGateConditionDto> conditions = getConditions(dbSession, qualityGate); Collection<QualityGateConditionDto> conditions = getConditions(dbSession, qualityGate);
Map<String, MetricDto> metricsByUuid = getMetricsByUuid(dbSession, conditions); Map<String, MetricDto> metricsByUuid = getMetricsByUuid(dbSession, conditions);
QualityGateDto defaultQualityGate = qualityGateFinder.getDefault(dbSession); 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);
} }
} }


} }


private ShowWsResponse buildResponse(DbSession dbSession, QualityGateDto qualityGate, QualityGateDto defaultQualityGate, Collection<QualityGateConditionDto> conditions, 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() return ShowWsResponse.newBuilder()
.setId(qualityGate.getUuid()) .setId(qualityGate.getUuid())
.setName(qualityGate.getName()) .setName(qualityGate.getName())
.setIsBuiltIn(qualityGate.isBuiltIn()) .setIsBuiltIn(qualityGate.isBuiltIn())
.setIsCaycCompliant(isCaycCompliant)
.setCaycStatus(caycStatus.toString())
.addAllConditions(conditions.stream() .addAllConditions(conditions.stream()
.map(toWsCondition(metricsByUuid)) .map(toWsCondition(metricsByUuid))
.collect(toList())) .collect(toList()))

+ 1
- 1
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/project_status-example.json View File

"projectStatus": { "projectStatus": {
"status": "ERROR", "status": "ERROR",
"ignoredConditions": false, "ignoredConditions": false,
"isCaycCompliant": false,
"caycStatus": "non-compliant",
"conditions": [ "conditions": [
{ {
"status": "ERROR", "status": "ERROR",

+ 9
- 6
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateCaycCheckerTest.java View File

import org.sonar.db.metric.MetricDto; import org.sonar.db.metric.MetricDto;
import org.sonar.db.qualitygate.QualityGateConditionDto; 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.DUPLICATED_LINES;
import static org.sonar.api.measures.CoreMetrics.LINE_COVERAGE; import static org.sonar.api.measures.CoreMetrics.LINE_COVERAGE;
import static org.sonar.api.measures.CoreMetrics.NEW_COVERAGE; import static org.sonar.api.measures.CoreMetrics.NEW_COVERAGE;
import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED; 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.api.measures.CoreMetrics.NEW_SECURITY_RATING;
import static org.sonar.server.qualitygate.QualityGateCaycChecker.CAYC_METRICS; 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 { public class QualityGateCaycCheckerTest {


public void checkCaycCompliant() { public void checkCaycCompliant() {
String qualityGateUuid = "abcd"; String qualityGateUuid = "abcd";
CAYC_METRICS.forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue())); 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 @Test
// extra conditions outside of CAYC requirements // extra conditions outside of CAYC requirements
List.of(LINE_COVERAGE, DUPLICATED_LINES).forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue())); 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 @Test
var metric = metrics.get(i); var metric = metrics.get(i);
insertCondition(metric, qualityGateUuid, idx == i ? metric.getWorstValue() : metric.getBestValue()); insertCondition(metric, qualityGateUuid, idx == i ? metric.getWorstValue() : metric.getBestValue());
} }
assertThat(underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)).isFalse();
assertEquals(NON_COMPLIANT, underTest.checkCaycCompliant(db.getSession(), qualityGateUuid));
}); });
} }


String qualityGateUuid = "abcd"; String qualityGateUuid = "abcd";
List.of(NEW_MAINTAINABILITY_RATING, NEW_RELIABILITY_RATING, NEW_SECURITY_HOTSPOTS_REVIEWED, NEW_DUPLICATED_LINES_DENSITY) List.of(NEW_MAINTAINABILITY_RATING, NEW_RELIABILITY_RATING, NEW_SECURITY_HOTSPOTS_REVIEWED, NEW_DUPLICATED_LINES_DENSITY)
.forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue())); .forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue()));
assertThat(underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)).isFalse();
assertEquals(NON_COMPLIANT, underTest.checkCaycCompliant(db.getSession(), qualityGateUuid));
} }


@Test @Test
.forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue())); .forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue()));
List.of(NEW_COVERAGE, NEW_DUPLICATED_LINES_DENSITY) List.of(NEW_COVERAGE, NEW_DUPLICATED_LINES_DENSITY)
.forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getWorstValue())); .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) { private void insertCondition(MetricDto metricDto, String qualityGateUuid, Double threshold) {

+ 18
- 6
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ListActionTest.java View File

*/ */
package org.sonar.server.qualitygate.ws; package org.sonar.server.qualitygate.ws;


import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService;
import static org.mockito.Mockito.when; 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_GATES;
import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES; 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.sonar.test.JsonAssert.assertJson;
import static org.sonarqube.ws.Qualitygates.ListWsResponse; import static org.sonarqube.ws.Qualitygates.ListWsResponse;


private final WsActionTester ws = new WsActionTester(new ListAction(db.getDbClient(), private final WsActionTester ws = new WsActionTester(new ListAction(db.getDbClient(),
new QualityGatesWsSupport(dbClient, userSession, TestComponentFinder.from(db)), qualityGateFinder, qualityGateCaycChecker)); new QualityGatesWsSupport(dbClient, userSession, TestComponentFinder.from(db)), qualityGateFinder, qualityGateCaycChecker));


@Before
public void setUp() {
when(qualityGateCaycChecker.checkCaycCompliant(any(), any())).thenReturn(COMPLIANT);
}

@Test @Test
public void list_quality_gates() { public void list_quality_gates() {
QualityGateDto defaultQualityGate = db.qualityGates().insertQualityGate(); QualityGateDto defaultQualityGate = db.qualityGates().insertQualityGate();
} }


@Test @Test
public void test_isCaycCompliant_flag() {
public void test_caycStatus_flag() {
QualityGateDto qualityGate1 = db.qualityGates().insertQualityGate(); QualityGateDto qualityGate1 = db.qualityGates().insertQualityGate();
QualityGateDto qualityGate2 = 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); db.qualityGates().setDefaultQualityGate(qualityGate1);


.executeProtobuf(ListWsResponse.class); .executeProtobuf(ListWsResponse.class);


assertThat(response.getQualitygatesList()) assertThat(response.getQualitygatesList())
.extracting(QualityGate::getId, QualityGate::getIsCaycCompliant)
.extracting(QualityGate::getId, QualityGate::getCaycStatus)
.containsExactlyInAnyOrder( .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 @Test

+ 11
- 3
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ProjectStatusActionTest.java View File

import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.RandomStringUtils;
import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.CoreMetrics;
import org.sonar.db.component.SnapshotDto; import org.sonar.db.component.SnapshotDto;
import org.sonar.db.metric.MetricDto; import org.sonar.db.metric.MetricDto;
import org.sonar.db.permission.GlobalPermission; import org.sonar.db.permission.GlobalPermission;
import org.sonar.db.qualitygate.QualityGateDto;
import org.sonar.server.component.TestComponentFinder; import org.sonar.server.component.TestComponentFinder;
import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.ForbiddenException;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.tuple; 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.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.sonar.db.measure.MeasureTesting.newLiveMeasure; import static org.sonar.db.measure.MeasureTesting.newLiveMeasure;
import static org.sonar.db.measure.MeasureTesting.newMeasureDto; import static org.sonar.db.measure.MeasureTesting.newMeasureDto;
import static org.sonar.db.metric.MetricTesting.newMetricDto; 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_ANALYSIS_ID;
import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_BRANCH; import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_BRANCH;
import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_ID; import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_ID;


private final WsActionTester ws = new WsActionTester(new ProjectStatusAction(dbClient, TestComponentFinder.from(db), userSession, qualityGateCaycChecker)); 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 @Test
public void test_definition() { public void test_definition() {
WebService.Action action = ws.getDef(); WebService.Action action = ws.getDef();
ComponentDto project = db.components().insertPrivateProject(); ComponentDto project = db.components().insertPrivateProject();
var qg = db.qualityGates().insertBuiltInQualityGate(); var qg = db.qualityGates().insertBuiltInQualityGate();
db.qualityGates().setDefaultQualityGate(qg); 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)); SnapshotDto snapshot = dbClient.snapshotDao().insert(dbSession, newAnalysis(project));
dbSession.commit(); dbSession.commit();
userSession.addProjectPermission(UserRole.USER, project); userSession.addProjectPermission(UserRole.USER, project);
.setParam(PARAM_ANALYSIS_ID, snapshot.getUuid()) .setParam(PARAM_ANALYSIS_ID, snapshot.getUuid())
.executeProtobuf(ProjectStatusResponse.class); .executeProtobuf(ProjectStatusResponse.class);


assertThat(result.getProjectStatus().getIsCaycCompliant()).isTrue();
assertEquals(COMPLIANT.toString(), result.getProjectStatus().getCaycStatus());
} }


@Test @Test

+ 4
- 3
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest.java View File



import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.junit.Test; import org.junit.Test;


import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; 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.api.utils.DateUtils.formatDateTime;
import static org.sonar.server.qualitygate.QualityGateCaycStatus.NON_COMPLIANT;


public class QualityGateDetailsFormatterTest { public class QualityGateDetailsFormatterTest {


ProjectStatus result = underTest.format(); ProjectStatus result = underTest.format();


assertThat(result.getStatus()).isEqualTo(ProjectStatusResponse.Status.ERROR); assertThat(result.getStatus()).isEqualTo(ProjectStatusResponse.Status.ERROR);
assertThat(result.getIsCaycCompliant()).isFalse();
assertEquals(NON_COMPLIANT.toString(), result.getCaycStatus());
// check conditions // check conditions
assertThat(result.getConditionsCount()).isEqualTo(3); assertThat(result.getConditionsCount()).isEqualTo(3);
List<ProjectStatusResponse.Condition> conditions = result.getConditionsList(); List<ProjectStatusResponse.Condition> conditions = result.getConditionsList();
} }


private static QualityGateDetailsFormatter newQualityGateDetailsFormatter(@Nullable String measureData, @Nullable SnapshotDto snapshotDto) { private static QualityGateDetailsFormatter newQualityGateDetailsFormatter(@Nullable String measureData, @Nullable SnapshotDto snapshotDto) {
return new QualityGateDetailsFormatter(measureData, snapshotDto, false);
return new QualityGateDetailsFormatter(measureData, snapshotDto, NON_COMPLIANT);
} }
} }

+ 10
- 2
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ShowActionTest.java View File

*/ */
package org.sonar.server.qualitygate.ws; package org.sonar.server.qualitygate.ws;


import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.AssertionsForClassTypes.tuple; 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.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; 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_GATES;
import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES; 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.sonar.test.JsonAssert.assertJson;
import static org.sonarqube.ws.Qualitygates.Actions; import static org.sonarqube.ws.Qualitygates.Actions;


new ShowAction(db.getDbClient(), new QualityGateFinder(db.getDbClient()), new ShowAction(db.getDbClient(), new QualityGateFinder(db.getDbClient()),
new QualityGatesWsSupport(db.getDbClient(), userSession, TestComponentFinder.from(db)), qualityGateCaycChecker)); new QualityGatesWsSupport(db.getDbClient(), userSession, TestComponentFinder.from(db)), qualityGateCaycChecker));


@Before
public void setUp() {
when(qualityGateCaycChecker.checkCaycCompliant(any(), any())).thenReturn(COMPLIANT);
}

@Test @Test
public void show() { public void show() {
QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); QualityGateDto qualityGate = db.qualityGates().insertQualityGate();
@Test @Test
public void show_isCaycCompliant() { public void show_isCaycCompliant() {
QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); 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); db.qualityGates().setDefaultQualityGate(qualityGate);


ShowWsResponse response = ws.newRequest() ShowWsResponse response = ws.newRequest()
.setParam("name", qualityGate.getName()) .setParam("name", qualityGate.getName())
.executeProtobuf(ShowWsResponse.class); .executeProtobuf(ShowWsResponse.class);


assertThat(response.getIsCaycCompliant()).isTrue();
assertEquals(COMPLIANT.toString(), response.getCaycStatus());
} }


@Test @Test

+ 18
- 4
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

quality_gates.cayc_condition.review_update=Review and Fix Quality Gate 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.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.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.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_maintainability_rating=Technical debt ratio is greater than {1}
quality_gates.cayc.new_reliability_rating.A=No bugs quality_gates.cayc.new_reliability_rating.A=No bugs
quality_gates.cayc.unlock_edit=Unlock editing 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.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.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.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.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 quality_gates.cayc.banner.list_item1=No bugs
quality_gates.cayc.banner.list_item3=No security hotspots to review 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_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.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


#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
# #
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_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.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.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.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.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.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_gate.hide_project_conditions_x=Hide failed conditions for project {0}
overview.quality_profiles=Quality Profiles used overview.quality_profiles=Quality Profiles used

+ 3
- 3
sonar-ws/src/main/protobuf/ws-qualitygates.proto View File

repeated Period periods = 3; repeated Period periods = 3;
optional bool ignoredConditions = 4; optional bool ignoredConditions = 4;
optional NewCodePeriod period = 5; optional NewCodePeriod period = 5;
optional bool isCaycCompliant = 6;
optional string caycStatus = 6;
} }


message Condition { message Condition {
repeated Condition conditions = 3; repeated Condition conditions = 3;
optional bool isBuiltIn = 4; optional bool isBuiltIn = 4;
optional Actions actions = 5; optional Actions actions = 5;
optional bool isCaycCompliant = 6;
optional string caycStatus = 6;


message Condition { message Condition {
optional string id = 1; optional string id = 1;
optional bool isDefault = 3; optional bool isDefault = 3;
optional bool isBuiltIn = 4; optional bool isBuiltIn = 4;
optional Actions actions = 5; optional Actions actions = 5;
optional bool isCaycCompliant = 6;
optional string caycStatus = 6;
} }


message RootActions { message RootActions {

Loading…
Cancel
Save