@@ -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); | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -35,7 +35,7 @@ public class DbVersion76Test { | |||
@Test | |||
public void verify_migration_count() { | |||
verifyMigrationCount(underTest, 2); | |||
verifyMigrationCount(underTest, 3); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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, | |||
); |