]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-17815 implement updated logic for CaYC quality gates
authorMatteo Mara <matteo.mara@sonarsource.com>
Fri, 27 Jan 2023 15:16:07 +0000 (16:16 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 2 Feb 2023 20:03:40 +0000 (20:03 +0000)
44 files changed:
server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryData.java
server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryDataJsonWriter.java
server/sonar-server-common/src/test/java/org/sonar/server/telemetry/TelemetryDataJsonWriterTest.java
server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts
server/sonar-web/src/main/js/apps/overview/branches/ApplicationNonCaycProjectWarning.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarning.tsx
server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarningOverCompliant.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/QualityGatePanelSection-test.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanel-test.tsx.snap
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanelSection-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-gates/components/CaycOverCompliantBadgeTooltip.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValue.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
server/sonar-web/src/main/js/apps/quality-gates/styles.css
server/sonar-web/src/main/js/apps/quality-gates/utils.ts
server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts
server/sonar-web/src/main/js/types/quality-gates.ts
server/sonar-web/src/main/js/types/types.ts
server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java
server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDataLoaderImplTest.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycChecker.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycStatus.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ListAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ProjectStatusAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ShowAction.java
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/project_status-example.json
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateCaycCheckerTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ListActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ProjectStatusActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ShowActionTest.java
sonar-core/src/main/resources/org/sonar/l10n/core.properties
sonar-ws/src/main/protobuf/ws-qualitygates.proto

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