]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11577 Migrate no more supported quality gate conditions
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Tue, 18 Dec 2018 17:46:37 +0000 (18:46 +0100)
committerSonarTech <sonartech@sonarsource.com>
Tue, 8 Jan 2019 19:21:07 +0000 (20:21 +0100)
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/MigrateNoMoreUsedQualityGateConditions.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76Test.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/MigrateNoMoreUsedQualityGateConditionsTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v76/MigrateNoMoreUsedQualityGateConditionsTest/qg-schema.sql [new file with mode: 0644]

index 84b16032fc227d068c0a0114309677f93b5000d5..8d7a7896bc4510775a0b04c20614b245fc2a5278 100644 (file)
@@ -28,6 +28,7 @@ public class DbVersion76 implements DbVersion {
   public void addSteps(MigrationStepRegistry registry) {
     registry
       .add(2500, "Create table USER_PROPERTIES", CreateUserPropertiesTable.class)
-      .add(2501, "Add index in table USER_PROPERTIES", AddUniqueIndexInUserPropertiesTable.class);
+      .add(2501, "Add index in table USER_PROPERTIES", AddUniqueIndexInUserPropertiesTable.class)
+      .add(2506, "Migrate quality gate conditions using warning, period and no more supported operations", MigrateNoMoreUsedQualityGateConditions.class);
   }
 }
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/MigrateNoMoreUsedQualityGateConditions.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/MigrateNoMoreUsedQualityGateConditions.java
new file mode 100644 (file)
index 0000000..ac2a5bf
--- /dev/null
@@ -0,0 +1,427 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.platform.db.migration.version.v76;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.SupportsBlueGreen;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.Upsert;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static java.util.stream.Collectors.toSet;
+import static org.sonar.core.util.stream.MoreCollectors.toList;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+
+@SupportsBlueGreen
+public class MigrateNoMoreUsedQualityGateConditions extends DataChange {
+
+  private static final Logger LOG = Loggers.get(MigrateNoMoreUsedQualityGateConditions.class);
+
+  private static final String OPERATOR_GREATER_THAN = "GT";
+  private static final String OPERATOR_LESS_THAN = "LT";
+
+  private static final int DIRECTION_WORST = -1;
+  private static final int DIRECTION_BETTER = 1;
+  private static final int DIRECTION_NONE = 0;
+
+  private static final Set<String> SUPPORTED_OPERATORS = ImmutableSet.of(OPERATOR_GREATER_THAN, OPERATOR_LESS_THAN);
+  private static final Set<String> SUPPORTED_METRIC_TYPES = ImmutableSet.of("INT", "FLOAT", "PERCENT", "MILLISEC", "LEVEL", "RATING", "WORK_DUR");
+  private static final Map<String, String> LEAK_METRIC_KEY_BY_METRIC_KEY = ImmutableMap.<String, String>builder()
+    .put("branch_coverage", "new_branch_coverage")
+    .put("conditions_to_cover", "new_conditions_to_cover")
+    .put("coverage", "new_coverage")
+    .put("line_coverage", "new_line_coverage")
+    .put("lines_to_cover", "new_lines_to_cover")
+    .put("uncovered_conditions", "new_uncovered_conditions")
+    .put("uncovered_lines", "new_uncovered_lines")
+    .put("duplicated_blocks", "new_duplicated_blocks")
+    .put("duplicated_lines", "new_duplicated_lines")
+    .put("duplicated_lines_density", "new_duplicated_lines_density")
+    .put("blocker_violations", "new_blocker_violations")
+    .put("critical_violations", "new_critical_violations")
+    .put("info_violations", "new_info_violations")
+    .put("violations", "new_violations")
+    .put("major_violations", "new_major_violations")
+    .put("minor_violations", "new_minor_violations")
+    .put("sqale_index", "new_technical_debt")
+    .put("code_smells", "new_code_smells")
+    .put("sqale_rating", "new_maintainability_rating")
+    .put("sqale_debt_ratio", "new_sqale_debt_ratio")
+    .put("bugs", "new_bugs")
+    .put("reliability_rating", "new_reliability_rating")
+    .put("reliability_remediation_effort", "new_reliability_remediation_effort")
+    .put("vulnerabilities", "new_vulnerabilities")
+    .put("security_rating", "new_security_rating")
+    .put("security_remediation_effort", "new_security_remediation_effort")
+    .put("lines", "new_lines")
+    .build();
+
+  private final System2 system2;
+
+  public MigrateNoMoreUsedQualityGateConditions(Database db, System2 system2) {
+    super(db);
+    this.system2 = system2;
+  }
+
+  @Override
+  protected void execute(Context context) throws SQLException {
+    MigrationContext migrationContext = new MigrationContext(context, new Date(system2.now()), loadMetrics(context));
+    context.prepareSelect("SELECT id FROM quality_gates qg " +
+      "WHERE qg.is_built_in=?")
+      .setBoolean(1, false)
+      .scroll(row -> {
+        List<QualityGateCondition> conditions = loadConditions(context, row.getInt(1));
+
+        markNoMoreSupportedConditionsAsToBeDeleted(migrationContext, conditions);
+        markConditionsHavingOnlyWarningAsToBeDeleted(conditions);
+        markConditionsUsingLeakPeriodHavingNoRelatedLeakMetricAsToBeDeleted(migrationContext, conditions);
+        markConditionsUsingLeakPeriodHavingAlreadyRelatedConditionAsToBeDeleted(migrationContext, conditions);
+        updateConditionsUsingLeakPeriod(migrationContext, conditions);
+        updateConditionsHavingErrorAndWarningByRemovingWarning(migrationContext, conditions);
+        dropConditionsIfNeeded(migrationContext, conditions);
+
+        migrationContext.increaseNumberOfProcessedQualityGate();
+      });
+    LOG.info("{} custom quality gates have been loaded", migrationContext.getNbOfQualityGates());
+    LOG.info("{} conditions have been removed", migrationContext.getNbOfRemovedConditions());
+    LOG.info("{} conditions have been updated", migrationContext.getNbOfUpdatedConditions());
+  }
+
+  private static List<Metric> loadMetrics(Context context) throws SQLException {
+    return context
+      .prepareSelect("SELECT m.id, m.name, m.val_type, m.direction FROM metrics m WHERE m.enabled=?")
+      .setBoolean(1, true)
+      .list(row -> new Metric(row.getInt(1), row.getString(2), row.getString(3), row.getInt(4)));
+  }
+
+  private static List<QualityGateCondition> loadConditions(Context context, int qualityGateId) throws SQLException {
+    return context.prepareSelect("SELECT qgc.id, qgc.metric_id, qgc.operator, qgc.value_error, qgc.value_warning, qgc.period FROM quality_gate_conditions qgc " +
+      "WHERE qgc.qgate_id=? ")
+      .setInt(1, qualityGateId)
+      .list(
+        row -> new QualityGateCondition(row.getInt(1), row.getInt(2), row.getString(3),
+          row.getString(4), row.getString(5), row.getInt(6)));
+  }
+
+  private static void markNoMoreSupportedConditionsAsToBeDeleted(MigrationContext migrationContext, List<QualityGateCondition> conditions) {
+    conditions.stream()
+      .filter(c -> !c.isToBeDeleted())
+      .filter(c -> !isConditionStillSupported(c, migrationContext.getMetricById(c.getMetricId())))
+      .forEach(QualityGateCondition::setToBeDeleted);
+  }
+
+  private static void markConditionsHavingOnlyWarningAsToBeDeleted(List<QualityGateCondition> conditions) {
+    conditions.stream()
+      .filter(c -> !c.isToBeDeleted())
+      .filter(c -> !isNullOrEmpty(c.getWarning()) && isNullOrEmpty(c.getError()))
+      .forEach(QualityGateCondition::setToBeDeleted);
+  }
+
+  private static void markConditionsUsingLeakPeriodHavingNoRelatedLeakMetricAsToBeDeleted(MigrationContext migrationContext, List<QualityGateCondition> conditions) {
+    conditions
+      .stream()
+      .filter(c -> !c.isToBeDeleted())
+      .filter(QualityGateCondition::hasLeakPeriod)
+      .filter(condition -> !isConditionOnLeakMetric(migrationContext, condition))
+      .forEach(condition -> {
+        String metricKey = migrationContext.getMetricById(condition.getMetricId()).getKey();
+        String relatedLeakMetric = LEAK_METRIC_KEY_BY_METRIC_KEY.get(metricKey);
+        // Metric has no related metric on leak period => delete condition
+        if (relatedLeakMetric == null) {
+          condition.setToBeDeleted();
+        }
+      });
+  }
+
+  private static void markConditionsUsingLeakPeriodHavingAlreadyRelatedConditionAsToBeDeleted(MigrationContext migrationContext, List<QualityGateCondition> conditions) {
+    Map<String, QualityGateCondition> conditionsByMetricKey = conditions.stream()
+      .filter(c -> !c.isToBeDeleted())
+      .collect(uniqueIndex(c -> migrationContext.getMetricById(c.getMetricId()).getKey()));
+
+    conditions
+      .stream()
+      .filter(condition -> !condition.isToBeDeleted())
+      .filter(QualityGateCondition::hasLeakPeriod)
+      .filter(condition -> !isConditionOnLeakMetric(migrationContext, condition))
+      .forEach(condition -> {
+        String metricKey = migrationContext.getMetricById(condition.getMetricId()).getKey();
+        String relatedLeakMetric = LEAK_METRIC_KEY_BY_METRIC_KEY.get(metricKey);
+        if (relatedLeakMetric != null) {
+          QualityGateCondition existingConditionUsingRelatedLeakPeriod = conditionsByMetricKey.get(relatedLeakMetric);
+          if (existingConditionUsingRelatedLeakPeriod != null) {
+            // Another condition on related leak period metric exist => delete condition
+            condition.setToBeDeleted();
+          }
+        }
+      });
+  }
+
+  private static void updateConditionsHavingErrorAndWarningByRemovingWarning(MigrationContext migrationContext, List<QualityGateCondition> conditions)
+    throws SQLException {
+    Set<Integer> conditionsToBeUpdated = conditions.stream()
+      .filter(c -> !c.isToBeDeleted())
+      .filter(c -> !isNullOrEmpty(c.getWarning()) && !isNullOrEmpty(c.getError()))
+      .map(QualityGateCondition::getId)
+      .collect(toSet());
+    if (conditionsToBeUpdated.isEmpty()) {
+      return;
+    }
+    migrationContext.getContext()
+      .prepareUpsert("UPDATE quality_gate_conditions SET value_warning = NULL, updated_at = ? WHERE id IN (" + conditionsToBeUpdated
+        .stream()
+        .map(c -> Integer.toString(c))
+        .collect(Collectors.joining(",")) + ")")
+      .setDate(1, migrationContext.getNow())
+      .execute()
+      .commit();
+    migrationContext.addUpdatedConditions(conditionsToBeUpdated.size());
+  }
+
+  private static void updateConditionsUsingLeakPeriod(MigrationContext migrationContext, List<QualityGateCondition> conditions)
+    throws SQLException {
+
+    Map<String, QualityGateCondition> conditionsByMetricKey = conditions.stream()
+      .filter(c -> !c.isToBeDeleted())
+      .collect(uniqueIndex(c -> migrationContext.getMetricById(c.getMetricId()).getKey()));
+
+    Upsert updateMetricId = migrationContext.getContext()
+      .prepareUpsert("UPDATE quality_gate_conditions SET metric_id = ?, updated_at = ? WHERE id = ? ")
+      .setDate(2, migrationContext.getNow());
+
+    conditions
+      .stream()
+      .filter(c -> !c.isToBeDeleted())
+      .filter(QualityGateCondition::hasLeakPeriod)
+      .filter(condition -> !isConditionOnLeakMetric(migrationContext, condition))
+      .forEach(condition -> {
+        String metricKey = migrationContext.getMetricById(condition.getMetricId()).getKey();
+        String relatedLeakMetric = LEAK_METRIC_KEY_BY_METRIC_KEY.get(metricKey);
+        QualityGateCondition existingConditionUsingRelatedLeakPeriod = conditionsByMetricKey.get(relatedLeakMetric);
+        // Metric has a related leak period metric => update the condition
+        if (existingConditionUsingRelatedLeakPeriod == null) {
+          try {
+            updateMetricId.setInt(1, migrationContext.getMetricByKey(relatedLeakMetric).getId());
+            updateMetricId.setInt(3, condition.getId());
+            updateMetricId.execute();
+            migrationContext.addUpdatedConditions(1);
+          } catch (SQLException e) {
+            throw new IllegalStateException("Fail to update quality gate conditions", e);
+          }
+        }
+      });
+    updateMetricId.commit();
+  }
+
+  private static void dropConditionsIfNeeded(MigrationContext context, List<QualityGateCondition> conditions) throws SQLException {
+    List<QualityGateCondition> conditionsToBeDeleted = conditions.stream()
+      .filter(QualityGateCondition::isToBeDeleted)
+      .collect(toList());
+    if (conditionsToBeDeleted.isEmpty()) {
+      return;
+    }
+    context.getContext()
+      .prepareUpsert("DELETE FROM quality_gate_conditions WHERE id IN (" + conditionsToBeDeleted
+        .stream()
+        .map(c -> Integer.toString(c.getId()))
+        .collect(Collectors.joining(",")) + ")")
+      .execute()
+      .commit();
+    context.addRemovedConditions(conditionsToBeDeleted.size());
+  }
+
+  private static boolean isConditionOnLeakMetric(MigrationContext migrationContext, QualityGateCondition condition) {
+    return LEAK_METRIC_KEY_BY_METRIC_KEY.containsValue(migrationContext.getMetricById(condition.getMetricId()).getKey());
+  }
+
+  private static boolean isConditionStillSupported(QualityGateCondition condition, Metric metric) {
+    return isSupportedMetricType(metric) && isSupportedOperator(condition, metric);
+  }
+
+  private static boolean isSupportedMetricType(Metric metric) {
+    return SUPPORTED_METRIC_TYPES.contains(metric.getType());
+  }
+
+  private static boolean isSupportedOperator(QualityGateCondition condition, Metric metric) {
+    String operator = condition.getOperator();
+    int direction = metric.getDirection();
+    return SUPPORTED_OPERATORS.contains(operator) &&
+      (direction == DIRECTION_NONE ||
+        (direction == DIRECTION_WORST && operator.equalsIgnoreCase(OPERATOR_GREATER_THAN)) ||
+        (direction == DIRECTION_BETTER && operator.equalsIgnoreCase(OPERATOR_LESS_THAN)));
+  }
+
+  private static class QualityGateCondition {
+    private final int id;
+    private final int metricId;
+    private final String operator;
+    private final String error;
+    private final String warning;
+    private final Integer period;
+
+    private boolean toBeDeleted = false;
+
+    public QualityGateCondition(int id, int metricId, String operator, @Nullable String error, @Nullable String warning,
+      @Nullable Integer period) {
+      this.id = id;
+      this.metricId = metricId;
+      this.operator = operator;
+      this.error = error;
+      this.warning = warning;
+      this.period = period;
+    }
+
+    public int getId() {
+      return id;
+    }
+
+    public int getMetricId() {
+      return metricId;
+    }
+
+    public String getOperator() {
+      return operator;
+    }
+
+    @CheckForNull
+    public String getError() {
+      return error;
+    }
+
+    @CheckForNull
+    public String getWarning() {
+      return warning;
+    }
+
+    public boolean hasLeakPeriod() {
+      return period != null && period == 1;
+    }
+
+    public void setToBeDeleted() {
+      toBeDeleted = true;
+    }
+
+    public boolean isToBeDeleted() {
+      return toBeDeleted;
+    }
+  }
+
+  private static class Metric {
+    private final int id;
+    private final String key;
+    private final String type;
+    private final int direction;
+
+    public Metric(int id, String key, String type, int direction) {
+      this.id = id;
+      this.key = key;
+      this.type = type;
+      this.direction = direction;
+    }
+
+    public int getId() {
+      return id;
+    }
+
+    public String getKey() {
+      return key;
+    }
+
+    public String getType() {
+      return type;
+    }
+
+    public int getDirection() {
+      return direction;
+    }
+  }
+
+  private static class MigrationContext {
+
+    private final Context context;
+    private final Date now;
+    private final Map<Integer, Metric> metricsById;
+    private final Map<String, Metric> metricsByKey;
+
+    private int nbOfQualityGates;
+    private int nbOfRemovedConditions;
+    private int nbOfUpdatedConditions;
+
+    public MigrationContext(Context context, Date now, List<Metric> metrics) {
+      this.context = context;
+      this.now = now;
+      this.metricsById = metrics.stream().collect(uniqueIndex(Metric::getId));
+      this.metricsByKey = metrics.stream().collect(uniqueIndex(Metric::getKey));
+    }
+
+    public Context getContext() {
+      return context;
+    }
+
+    public Date getNow() {
+      return now;
+    }
+
+    public Metric getMetricByKey(String key) {
+      return metricsByKey.get(key);
+    }
+
+    public Metric getMetricById(int id) {
+      return metricsById.get(id);
+    }
+
+    public void increaseNumberOfProcessedQualityGate() {
+      nbOfQualityGates += 1;
+    }
+
+    public int getNbOfQualityGates() {
+      return nbOfQualityGates;
+    }
+
+    public void addRemovedConditions(int removedConditions) {
+      nbOfRemovedConditions += removedConditions;
+    }
+
+    public int getNbOfRemovedConditions() {
+      return nbOfRemovedConditions;
+    }
+
+    public void addUpdatedConditions(int updatedConditions) {
+      nbOfUpdatedConditions += updatedConditions;
+    }
+
+    public int getNbOfUpdatedConditions() {
+      return nbOfUpdatedConditions;
+    }
+  }
+
+}
index 6316e8c536baf5504a29496284b358c19215ee34..88ba43ce5330d7ce640a5b1e2453a5d7d9b49428 100644 (file)
@@ -35,7 +35,7 @@ public class DbVersion76Test {
 
   @Test
   public void verify_migration_count() {
-    verifyMigrationCount(underTest, 2);
+    verifyMigrationCount(underTest, 3);
   }
 
 }
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/MigrateNoMoreUsedQualityGateConditionsTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/MigrateNoMoreUsedQualityGateConditionsTest.java
new file mode 100644 (file)
index 0000000..c725577
--- /dev/null
@@ -0,0 +1,311 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.platform.db.migration.version.v76;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.Random;
+import javax.annotation.Nullable;
+import org.assertj.core.groups.Tuple;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.internal.TestSystem2;
+import org.sonar.db.CoreDbTester;
+
+import static java.util.stream.Collectors.toList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+
+@RunWith(DataProviderRunner.class)
+public class MigrateNoMoreUsedQualityGateConditionsTest {
+
+  private static final int DIRECTION_WORST = -1;
+  private static final int DIRECTION_BETTER = 1;
+  private static final int DIRECTION_NONE = 0;
+
+  private final static long PAST = 10_000_000_000L;
+  private final static long NOW = 50_000_000_000L;
+
+  @Rule
+  public CoreDbTester db = CoreDbTester.createForSchema(MigrateNoMoreUsedQualityGateConditionsTest.class, "qg-schema.sql");
+
+  private System2 system2 = new TestSystem2().setNow(NOW);
+
+  private Random random = new Random();
+
+  private MigrateNoMoreUsedQualityGateConditions underTest = new MigrateNoMoreUsedQualityGateConditions(db.database(), system2);
+
+  @Test
+  public void remove_conditions_using_only_warning() throws SQLException {
+    long qualityGate = insertQualityGate(false);
+    long ncloc = insertMetric("ncloc", DIRECTION_WORST, "INT");
+    long lines = insertMetric("lines", DIRECTION_WORST, "INT");
+    long issues = insertMetric("violations", DIRECTION_WORST, "INT");
+    long coverage = insertMetric("coverage", DIRECTION_BETTER, "PERCENT");
+    long conditionWithWarning1 = insertCondition(qualityGate, ncloc, "GT", null, "10", null);
+    long conditionWithWarning2 = insertCondition(qualityGate, lines, "GT", null, "15", null);
+    long conditionWithError = insertCondition(qualityGate, issues, "GT", "5", null, null);
+
+    underTest.execute();
+
+    assertConditions(
+      tuple(conditionWithError, issues, "5", null, null));
+  }
+
+  @Test
+  public void update_conditions_using_error_and_warning() throws SQLException {
+    long qualityGate = insertQualityGate(false);
+    long issues = insertMetric("violations", DIRECTION_WORST, "INT");
+    long coverage = insertMetric("coverage", DIRECTION_BETTER, "PERCENT");
+    long newLines = insertMetric("new_lines", DIRECTION_WORST, "INT");
+    long conditionWithError = insertCondition(qualityGate, issues, "GT", "5", null, null);
+    long conditionWithErrorAndWarning1 = insertCondition(qualityGate, coverage, "LT", "5", "10", null);
+    long conditionWithErrorAndWarning2 = insertCondition(qualityGate, newLines, "GT", "7", "13", 1);
+
+    underTest.execute();
+
+    assertConditions(
+      tuple(conditionWithError, issues, "5", null, null),
+      tuple(conditionWithErrorAndWarning1, coverage, "5", null, null),
+      tuple(conditionWithErrorAndWarning2, newLines, "7", null, 1L)
+    );
+  }
+
+  @Test
+  @UseDataProvider("metricsHavingNoLinkedLeakMetrics")
+  public void delete_condition_on_not_supported_leak_period_metric(String metricKey) throws SQLException {
+    long qualityGate = insertQualityGate(false);
+    long metric = insertMetric(metricKey, DIRECTION_BETTER, "INT");
+    insertCondition(qualityGate, metric, "LT", "5", null, 1);
+
+    underTest.execute();
+
+    assertThat(db.countRowsOfTable("quality_gate_conditions")).isZero();
+  }
+
+  @DataProvider
+  public static Object[][] metricsHavingNoLinkedLeakMetrics() {
+    return new Object[][] {
+      {"statements"},
+      {"functions"}
+    };
+  }
+
+  @Test
+  @UseDataProvider("supportedLeakPeriodMetrics")
+  public void update_condition_on_supported_leak_period_metric(String metricKey, String relatedMetricKeyOnLeakPeriod) throws SQLException {
+    long qualityGate = insertQualityGate(false);
+    long metric = insertMetric(metricKey, DIRECTION_BETTER, "INT");
+    long relatedMetricOnLeakPeriod = insertMetric(relatedMetricKeyOnLeakPeriod, DIRECTION_BETTER, "INT");
+    long condition = insertCondition(qualityGate, metric, "LT", "5", null, 1);
+
+    underTest.execute();
+
+    assertConditions(tuple(condition, relatedMetricOnLeakPeriod, "5", null, 1L));
+  }
+
+  @Test
+  @UseDataProvider("supportedLeakPeriodMetrics")
+  public void remove_condition_on_supported_leak_period_metric_when_condition_already_exists(String metricKey, String relatedMetricKeyOnLeakPeriod) throws SQLException {
+    long qualityGate = insertQualityGate(false);
+    long metric = insertMetric(metricKey, DIRECTION_BETTER, "INT");
+    long condition = insertCondition(qualityGate, metric, "LT", "10", null, 1);
+    long relatedMetricOnLeakPeriod = insertMetric(relatedMetricKeyOnLeakPeriod, DIRECTION_BETTER, "INT");
+    long leakCondition = insertCondition(qualityGate, relatedMetricOnLeakPeriod, "LT", "5", null, 1);
+
+    underTest.execute();
+
+    assertConditions(tuple(leakCondition, relatedMetricOnLeakPeriod, "5", null, 1L));
+  }
+
+  @DataProvider
+  public static Object[][] supportedLeakPeriodMetrics() {
+    return new Object[][] {
+      {"branch_coverage", "new_branch_coverage"},
+      {"conditions_to_cover", "new_conditions_to_cover"},
+      {"coverage", "new_coverage"},
+      {"line_coverage", "new_line_coverage"},
+      {"lines_to_cover", "new_lines_to_cover"},
+      {"uncovered_conditions", "new_uncovered_conditions"},
+      {"uncovered_lines", "new_uncovered_lines"},
+      {"duplicated_blocks", "new_duplicated_blocks"},
+      {"duplicated_lines", "new_duplicated_lines"},
+      {"duplicated_lines_density", "new_duplicated_lines_density"},
+      {"blocker_violations", "new_blocker_violations"},
+      {"critical_violations", "new_critical_violations"},
+      {"info_violations", "new_info_violations"},
+      {"violations", "new_violations"},
+      {"major_violations", "new_major_violations"},
+      {"minor_violations", "new_minor_violations"},
+      {"sqale_index", "new_technical_debt"},
+      {"code_smells", "new_code_smells"},
+      {"sqale_rating", "new_maintainability_rating"},
+      {"sqale_debt_ratio", "new_sqale_debt_ratio"},
+      {"bugs", "new_bugs"},
+      {"reliability_rating", "new_reliability_rating"},
+      {"reliability_remediation_effort", "new_reliability_remediation_effort"},
+      {"vulnerabilities", "new_vulnerabilities"},
+      {"security_rating", "new_security_rating"},
+      {"security_remediation_effort", "new_security_remediation_effort"},
+      {"lines", "new_lines"},
+    };
+  }
+
+  @Test
+  public void update_condition_using_leak_period_metric_when_condition_on_new_metric_exists_but_using_bad_operator() throws SQLException {
+    long qualityGate = insertQualityGate(false);
+    long linesToCover = insertMetric("lines_to_cover", DIRECTION_WORST, "INT");
+    long newLinesToCover = insertMetric("new_lines_to_cover", DIRECTION_WORST, "INT");
+    // This condition should be migrated to use new_lines_to_cover metric
+    long conditionOnLinesToCover = insertCondition(qualityGate, linesToCover, "GT", "10", null, 1);
+    // This condition should be removed as using a no more supported operator
+    long conditionOnNewLinesToCover = insertCondition(qualityGate, newLinesToCover, "EQ", "5", null, 1);
+
+    underTest.execute();
+
+    assertConditions(
+      tuple(conditionOnLinesToCover, newLinesToCover, "10", null, 1L));
+  }
+
+  @Test
+  @UseDataProvider("noMoreSupportedMetricTypes")
+  public void delete_condition_on_no_more_supported_metric_types(String metricKey, String metricType) throws SQLException {
+    long qualityGate = insertQualityGate(false);
+    long metric = insertMetric(metricKey, DIRECTION_BETTER, metricType);
+    long condition = insertCondition(qualityGate, metric, "LT", "5", null, null);
+
+    underTest.execute();
+
+    assertThat(db.countRowsOfTable("quality_gate_conditions")).isZero();
+  }
+
+  @DataProvider
+  public static Object[][] noMoreSupportedMetricTypes() {
+    return new Object[][] {
+      {"bool_type", "BOOL"},
+      {"development_cost", "STRING"},
+      {"last_change_on_maintainability_rating", "DATA"},
+      {"class_complexity_distribution", "DISTRIB"}
+    };
+  }
+
+  @Test
+  @UseDataProvider("conditionsOnNoMoreSupportedOperators")
+  public void delete_condition_on_no_more_supported_operators(String metricKey, int direction, String operator) throws SQLException {
+    long qualityGate = insertQualityGate(false);
+    long metric = insertMetric(metricKey, direction, "INT");
+    long condition = insertCondition(qualityGate, metric, operator, "5", null, null);
+
+    underTest.execute();
+
+    assertThat(db.countRowsOfTable("quality_gate_conditions")).isZero();
+  }
+
+  @DataProvider
+  public static Object[][] conditionsOnNoMoreSupportedOperators() {
+    return new Object[][] {
+      {"function_complexity_distribution", DIRECTION_NONE, "EQ"},
+      {"file_complexity_distribution", DIRECTION_NONE, "NE"},
+      {"blockers", DIRECTION_WORST, "LT"},
+      {"coverage", DIRECTION_BETTER, "GT"}
+    };
+  }
+
+  @Test
+  @UseDataProvider("conditionsOnSupportedOperators")
+  public void do_not_delete_condition_on_supported_operators(String metricKey, int direction, String operator) throws SQLException {
+    long qualityGate = insertQualityGate(false);
+    long metric = insertMetric(metricKey, direction, "INT");
+    long condition = insertCondition(qualityGate, metric, operator, "5", null, null);
+
+    underTest.execute();
+
+    assertConditions(condition);
+  }
+
+  @DataProvider
+  public static Object[][] conditionsOnSupportedOperators() {
+    return new Object[][] {
+      {"function_complexity_distribution", DIRECTION_NONE, "LT"},
+      {"file_complexity_distribution", DIRECTION_NONE, "GT"},
+      {"blockers", DIRECTION_BETTER, "LT"},
+      {"coverage", DIRECTION_WORST, "GT"}
+    };
+  }
+
+  private void assertConditions(Long... expectedIds) {
+    assertThat(db.select("SELECT id FROM quality_gate_conditions")
+      .stream()
+      .map(row -> (long) row.get("ID"))
+      .collect(toList()))
+        .containsExactlyInAnyOrder(expectedIds);
+  }
+
+  private void assertConditions(Tuple... expectedTuples) {
+    assertThat(db.select("SELECT id, metric_id, value_error, value_warning, period FROM quality_gate_conditions")
+      .stream()
+      .map(row -> new Tuple(row.get("ID"), row.get("METRIC_ID"), row.get("VALUE_ERROR"), row.get("VALUE_WARNING"), row.get("PERIOD")))
+      .collect(toList()))
+        .containsExactlyInAnyOrder(expectedTuples);
+  }
+
+  private long insertQualityGate(boolean isBuiltIn) {
+    long id = random.nextInt(1_000_000);
+    db.executeInsert("QUALITY_GATES",
+      "UUID", id,
+      "ID", id,
+      "NAME", "name " + id,
+      "IS_BUILT_IN", isBuiltIn,
+      "CREATED_AT", new Date(PAST),
+      "UPDATED_AT", new Date(PAST));
+    return id;
+  }
+
+  private long insertMetric(String name, int direction, String metricType) {
+    long id = random.nextInt(1_000_000);
+    db.executeInsert("METRICS",
+      "ID", id,
+      "NAME", name,
+      "VAL_TYPE", metricType,
+      "DIRECTION", direction);
+    return id;
+  }
+
+  private long insertCondition(long qualityGateId, long metricId, String operator, @Nullable String error, @Nullable String warning, @Nullable Integer period) {
+    long id = random.nextInt(1_000_000);
+    db.executeInsert("QUALITY_GATE_CONDITIONS",
+      "ID", id,
+      "QGATE_ID", qualityGateId,
+      "METRIC_ID", metricId,
+      "OPERATOR", operator,
+      "VALUE_ERROR", error,
+      "VALUE_WARNING", warning,
+      "PERIOD", period,
+      "CREATED_AT", new Date(PAST),
+      "UPDATED_AT", new Date(PAST));
+    return id;
+  }
+
+}
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v76/MigrateNoMoreUsedQualityGateConditionsTest/qg-schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v76/MigrateNoMoreUsedQualityGateConditionsTest/qg-schema.sql
new file mode 100644 (file)
index 0000000..6082612
--- /dev/null
@@ -0,0 +1,41 @@
+CREATE TABLE "METRICS" (
+  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+  "NAME" VARCHAR(64) NOT NULL,
+  "DESCRIPTION" VARCHAR(255),
+  "DIRECTION" INTEGER NOT NULL DEFAULT 0,
+  "DOMAIN" VARCHAR(64),
+  "SHORT_NAME" VARCHAR(64),
+  "QUALITATIVE" BOOLEAN NOT NULL DEFAULT FALSE,
+  "VAL_TYPE" VARCHAR(8),
+  "USER_MANAGED" BOOLEAN DEFAULT FALSE,
+  "ENABLED" BOOLEAN DEFAULT TRUE,
+  "WORST_VALUE" DOUBLE,
+  "BEST_VALUE" DOUBLE,
+  "OPTIMIZED_BEST_VALUE" BOOLEAN,
+  "HIDDEN" BOOLEAN,
+  "DELETE_HISTORICAL_DATA" BOOLEAN,
+  "DECIMAL_SCALE" INTEGER
+);
+CREATE UNIQUE INDEX "METRICS_UNIQUE_NAME" ON "METRICS" ("NAME");
+
+CREATE TABLE "QUALITY_GATES" (
+  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+  "UUID" VARCHAR(40) NOT NULL,
+  "NAME" VARCHAR(100) NOT NULL,
+  "IS_BUILT_IN" BOOLEAN NOT NULL,
+  "CREATED_AT" TIMESTAMP,
+  "UPDATED_AT" TIMESTAMP,
+);
+CREATE UNIQUE INDEX "UNIQ_QUALITY_GATES_UUID" ON "QUALITY_GATES" ("UUID");
+
+CREATE TABLE "QUALITY_GATE_CONDITIONS" (
+  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+  "QGATE_ID" INTEGER,
+  "METRIC_ID" INTEGER,
+  "OPERATOR" VARCHAR(3),
+  "VALUE_ERROR" VARCHAR(64),
+  "VALUE_WARNING" VARCHAR(64),
+  "PERIOD" INTEGER,
+  "CREATED_AT" TIMESTAMP,
+  "UPDATED_AT" TIMESTAMP,
+);