From 4dddfec1850accb70b08e5da5f257b2625f557c8 Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Tue, 18 Dec 2018 18:46:37 +0100 Subject: [PATCH] SONAR-11577 Migrate no more supported quality gate conditions --- .../db/migration/version/v76/DbVersion76.java | 3 +- ...igrateNoMoreUsedQualityGateConditions.java | 427 ++++++++++++++++++ .../version/v76/DbVersion76Test.java | 2 +- ...teNoMoreUsedQualityGateConditionsTest.java | 311 +++++++++++++ .../qg-schema.sql | 41 ++ 5 files changed, 782 insertions(+), 2 deletions(-) create mode 100644 server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/MigrateNoMoreUsedQualityGateConditions.java create mode 100644 server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/MigrateNoMoreUsedQualityGateConditionsTest.java create mode 100644 server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v76/MigrateNoMoreUsedQualityGateConditionsTest/qg-schema.sql diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76.java index 84b16032fc2..8d7a7896bc4 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76.java @@ -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 index 00000000000..ac2a5bf101f --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/MigrateNoMoreUsedQualityGateConditions.java @@ -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 SUPPORTED_OPERATORS = ImmutableSet.of(OPERATOR_GREATER_THAN, OPERATOR_LESS_THAN); + private static final Set SUPPORTED_METRIC_TYPES = ImmutableSet.of("INT", "FLOAT", "PERCENT", "MILLISEC", "LEVEL", "RATING", "WORK_DUR"); + private static final Map LEAK_METRIC_KEY_BY_METRIC_KEY = ImmutableMap.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 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 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 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 conditions) { + conditions.stream() + .filter(c -> !c.isToBeDeleted()) + .filter(c -> !isConditionStillSupported(c, migrationContext.getMetricById(c.getMetricId()))) + .forEach(QualityGateCondition::setToBeDeleted); + } + + private static void markConditionsHavingOnlyWarningAsToBeDeleted(List 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 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 conditions) { + Map 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 conditions) + throws SQLException { + Set 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 conditions) + throws SQLException { + + Map 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 conditions) throws SQLException { + List 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 metricsById; + private final Map metricsByKey; + + private int nbOfQualityGates; + private int nbOfRemovedConditions; + private int nbOfUpdatedConditions; + + public MigrationContext(Context context, Date now, List 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; + } + } + +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76Test.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76Test.java index 6316e8c536b..88ba43ce533 100644 --- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76Test.java +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76Test.java @@ -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 index 00000000000..c725577ec8a --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/MigrateNoMoreUsedQualityGateConditionsTest.java @@ -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 index 00000000000..6082612e609 --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v76/MigrateNoMoreUsedQualityGateConditionsTest/qg-schema.sql @@ -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, +); -- 2.39.5