@@ -123,6 +123,7 @@ import org.sonar.ce.task.projectanalysis.qualitymodel.ReliabilityAndSecurityRati | |||
import org.sonar.ce.task.projectanalysis.qualitymodel.SecurityReviewMeasuresVisitor; | |||
import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolderImpl; | |||
import org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepositoryImpl; | |||
import org.sonar.ce.task.projectanalysis.qualityprofile.QualityProfileRuleChangeResolver; | |||
import org.sonar.ce.task.projectanalysis.scm.ScmInfoDbLoader; | |||
import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepositoryImpl; | |||
import org.sonar.ce.task.projectanalysis.source.DbLineHashVersion; | |||
@@ -251,6 +252,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop | |||
IssueFilter.class, | |||
FlowGenerator.class, | |||
QualityProfileRuleChangeResolver.class, | |||
// push events | |||
PushEventFactory.class, | |||
@@ -0,0 +1,139 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.ce.task.projectanalysis.qualityprofile; | |||
import java.util.AbstractMap; | |||
import java.util.Comparator; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.stream.Collectors; | |||
import org.jetbrains.annotations.NotNull; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.qualityprofile.QProfileChangeDto; | |||
import org.sonar.db.qualityprofile.QProfileChangeQuery; | |||
import org.sonar.server.qualityprofile.ActiveRuleChange; | |||
import org.sonar.server.qualityprofile.QualityProfile; | |||
import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED; | |||
import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.DEACTIVATED; | |||
import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.UPDATED; | |||
public class QualityProfileRuleChangeResolver { | |||
private final DbClient dbClient; | |||
public QualityProfileRuleChangeResolver(DbClient dbClient) { | |||
this.dbClient = dbClient; | |||
} | |||
/** | |||
* Returns a text description of the changes made to a quality profile. | |||
* The text is generated by looking at the last change for each rule and determining the final status of the rule. | |||
* For old taxonomy, we need to access the QProfileChangeDto.data field to determine the ruleUuid. | |||
* For CCT, we can use the QProfileChangeDto.ruleChange field to determine the ruleUuid. | |||
* | |||
* @param profile the quality profile to generate the text for | |||
* @return a text description of the changes made to the profile | |||
* @throws IllegalStateException if no changes are found for the profile | |||
*/ | |||
public Map<ActiveRuleChange.Type, Long> mapChangeToNumberOfRules(QualityProfile profile, String componentUuid) { | |||
// get profile changes | |||
List<QProfileChangeDto> profileChanges = getProfileChanges(profile, componentUuid); | |||
Map<String, List<QProfileChangeDto>> updatedRulesGrouped = profileChanges.stream() | |||
.filter(QualityProfileRuleChangeResolver::hasRuleUuid) | |||
.collect(Collectors.groupingBy(p -> p.getRuleChange() != null ? p.getRuleChange().getRuleUuid() : p.getDataAsMap().get("ruleUuid"))); | |||
Map<String, ActiveRuleChange.Type> rulesMappedToFinalChange = getRulesMappedToFinalChange(updatedRulesGrouped); | |||
return getChangeMappedToNumberOfRules(rulesMappedToFinalChange); | |||
} | |||
@NotNull | |||
private static Map<ActiveRuleChange.Type, Long> getChangeMappedToNumberOfRules(Map<String, ActiveRuleChange.Type> rulesMappedToFinalChange) { | |||
return rulesMappedToFinalChange.values().stream() | |||
.collect(Collectors.groupingBy( | |||
actionType -> actionType, | |||
Collectors.counting() | |||
)); | |||
} | |||
private static boolean hasRuleUuid(QProfileChangeDto change) { | |||
return (change.getRuleChange() != null && change.getRuleChange().getRuleUuid() != null) || | |||
(!change.getDataAsMap().isEmpty() && change.getDataAsMap().containsKey("ruleUuid")); | |||
} | |||
/** | |||
* Returns a map of ruleUuid to the final status of the rule. | |||
* If the rule final status is the same as the initial status, the value will be empty. | |||
* | |||
* @param updatedRulesGrouped a map of ruleUuid to a list of changes for that rule | |||
* @return a map of ruleUuid to the final status of the rule. If the rule final status is the same as the initial status, the value will be empty. | |||
*/ | |||
private static Map<String, ActiveRuleChange.Type> getRulesMappedToFinalChange(Map<String, List<QProfileChangeDto>> updatedRulesGrouped) { | |||
return updatedRulesGrouped.entrySet().stream() | |||
.map(entry -> { | |||
String key = entry.getKey(); | |||
List<QProfileChangeDto> ruleChanges = entry.getValue(); | |||
// get last change | |||
QProfileChangeDto lastChange = ruleChanges.stream().max(Comparator.comparing(QProfileChangeDto::getCreatedAt)).orElseThrow(); | |||
Optional<ActiveRuleChange.Type> value; | |||
if (UPDATED.name().equals(lastChange.getChangeType())) { | |||
value = Optional.of(UPDATED); | |||
} else { | |||
// for ACTIVATED/DEACTIVATED we need to count the number of times the rule was toggled | |||
long activationToggles = ruleChanges.stream() | |||
.filter(rule -> List.of(ACTIVATED.name(), DEACTIVATED.name()).contains(rule.getChangeType())) | |||
.count(); | |||
// If the count is even, skip all rules in this group as the status is unchanged | |||
// If the count is odd we only care about the last status update | |||
value = activationToggles % 2 == 0 ? Optional.empty() : Optional.of(ActiveRuleChange.Type.valueOf(lastChange.getChangeType())); | |||
} | |||
return new AbstractMap.SimpleEntry<>(key, value); | |||
}) | |||
.filter(entry -> entry.getValue().isPresent()) | |||
.collect(Collectors.toMap( | |||
Map.Entry::getKey, | |||
entry -> entry.getValue().get() | |||
)); | |||
} | |||
private List<QProfileChangeDto> getProfileChanges(QualityProfile profile, String componentUuid) { | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
QProfileChangeQuery query = new QProfileChangeQuery(profile.getQpKey()); | |||
query.setFromIncluded(getLastAnalysisDate(componentUuid, dbSession)); | |||
List<QProfileChangeDto> profileChanges = dbClient.qProfileChangeDao().selectByQuery(dbSession, query); | |||
if (profileChanges.isEmpty()) { | |||
throw new IllegalStateException("No profile changes found for " + profile.getQpName()); | |||
} | |||
return profileChanges; | |||
} | |||
} | |||
@NotNull | |||
private Long getLastAnalysisDate(String componentUuid, DbSession dbSession) { | |||
return dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, componentUuid) | |||
.orElseThrow(() -> new IllegalStateException("No snapshot found for " + componentUuid)).getAnalysisDate(); | |||
} | |||
} |
@@ -0,0 +1,74 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.ce.task.projectanalysis.qualityprofile; | |||
import java.util.Comparator; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.function.Function; | |||
import java.util.stream.Collectors; | |||
import org.sonar.server.qualityprofile.ActiveRuleChange; | |||
/** | |||
* Builder for generating a text description of the changes made to a quality profile. | |||
*/ | |||
public final class QualityProfileTextGenerator { | |||
private static final Map<ActiveRuleChange.Type, String> CHANGE_TO_TEXT_MAP = Map.ofEntries( | |||
Map.entry(ActiveRuleChange.Type.ACTIVATED, " new rule"), | |||
Map.entry(ActiveRuleChange.Type.DEACTIVATED, " deactivated rule"), | |||
Map.entry(ActiveRuleChange.Type.UPDATED, " modified rule") | |||
); | |||
private QualityProfileTextGenerator() { | |||
// only static methods | |||
} | |||
/** | |||
* Returns a text description of the changes made to a quality profile. Oxford comma is not used. | |||
* The order of the changes is based on the order of the enum name (activated, deactivated, updated) to keep consistency. | |||
* 0 values are filtered out. | |||
* | |||
* @param changesMappedToNumberOfRules the changes mapped to the number of rules | |||
* @return a text description of the changes made to the profile | |||
*/ | |||
public static String generateRuleChangeText(Map<ActiveRuleChange.Type, Long> changesMappedToNumberOfRules) { | |||
return changesMappedToNumberOfRules.entrySet().stream() | |||
.sorted(Map.Entry.comparingByKey(Comparator.comparing(Enum::name))) | |||
.filter(entry -> entry.getValue() > 0) | |||
.map(entry -> generateRuleText(entry.getValue(), CHANGE_TO_TEXT_MAP.get(entry.getKey()))) | |||
.collect(Collectors.collectingAndThen(Collectors.toList(), joiningLastDelimiter(", ", " and "))); | |||
} | |||
private static String generateRuleText(Long ruleNumber, String ruleText) { | |||
return ruleNumber + ruleText + (ruleNumber > 1 ? "s" : ""); | |||
} | |||
private static Function<List<String>, String> joiningLastDelimiter(String delimiter, String lastDelimiter) { | |||
return list -> { | |||
int last = list.size() - 1; | |||
if (last < 1) return String.join(delimiter, list); | |||
return String.join(lastDelimiter, | |||
String.join(delimiter, list.subList(0, last)), | |||
list.get(last)); | |||
}; | |||
} | |||
} |
@@ -26,6 +26,8 @@ import java.util.Map; | |||
import java.util.Optional; | |||
import javax.annotation.Nullable; | |||
import org.apache.commons.lang.time.DateUtils; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.api.measures.CoreMetrics; | |||
import org.sonar.api.resources.Language; | |||
import org.sonar.api.utils.KeyValueFormat; | |||
@@ -38,8 +40,11 @@ import org.sonar.ce.task.projectanalysis.measure.Measure; | |||
import org.sonar.ce.task.projectanalysis.measure.MeasureRepository; | |||
import org.sonar.ce.task.projectanalysis.metric.MetricRepository; | |||
import org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository; | |||
import org.sonar.ce.task.projectanalysis.qualityprofile.QualityProfileRuleChangeResolver; | |||
import org.sonar.ce.task.projectanalysis.qualityprofile.QualityProfileTextGenerator; | |||
import org.sonar.ce.task.step.ComputationStep; | |||
import org.sonar.core.util.UtcDateUtils; | |||
import org.sonar.server.qualityprofile.ActiveRuleChange; | |||
import org.sonar.server.qualityprofile.QPMeasureData; | |||
import org.sonar.server.qualityprofile.QualityProfile; | |||
@@ -52,22 +57,25 @@ import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRep | |||
* As it depends upon {@link CoreMetrics#QUALITY_PROFILES_KEY}, it must be executed after {@link ComputeQProfileMeasureStep} | |||
*/ | |||
public class QualityProfileEventsStep implements ComputationStep { | |||
private static final Logger LOG = LoggerFactory.getLogger(QualityProfileEventsStep.class); | |||
private final TreeRootHolder treeRootHolder; | |||
private final MetricRepository metricRepository; | |||
private final MeasureRepository measureRepository; | |||
private final EventRepository eventRepository; | |||
private final LanguageRepository languageRepository; | |||
private final QProfileStatusRepository qProfileStatusRepository; | |||
private final QualityProfileRuleChangeResolver qualityProfileRuleChangeTextResolver; | |||
public QualityProfileEventsStep(TreeRootHolder treeRootHolder, | |||
MetricRepository metricRepository, MeasureRepository measureRepository, LanguageRepository languageRepository, | |||
EventRepository eventRepository, QProfileStatusRepository qProfileStatusRepository) { | |||
EventRepository eventRepository, QProfileStatusRepository qProfileStatusRepository, QualityProfileRuleChangeResolver qualityProfileRuleChangeTextResolver) { | |||
this.treeRootHolder = treeRootHolder; | |||
this.metricRepository = metricRepository; | |||
this.measureRepository = measureRepository; | |||
this.eventRepository = eventRepository; | |||
this.languageRepository = languageRepository; | |||
this.qProfileStatusRepository = qProfileStatusRepository; | |||
this.qualityProfileRuleChangeTextResolver = qualityProfileRuleChangeTextResolver; | |||
} | |||
@Override | |||
@@ -77,21 +85,21 @@ public class QualityProfileEventsStep implements ComputationStep { | |||
private void executeForBranch(Component branchComponent) { | |||
Optional<Measure> baseMeasure = measureRepository.getBaseMeasure(branchComponent, metricRepository.getByKey(CoreMetrics.QUALITY_PROFILES_KEY)); | |||
if (!baseMeasure.isPresent()) { | |||
if (baseMeasure.isEmpty()) { | |||
// first analysis -> do not generate events | |||
return; | |||
} | |||
// Load profiles used in current analysis for which at least one file of the corresponding language exists | |||
Optional<Measure> rawMeasure = measureRepository.getRawMeasure(branchComponent, metricRepository.getByKey(CoreMetrics.QUALITY_PROFILES_KEY)); | |||
if (!rawMeasure.isPresent()) { | |||
if (rawMeasure.isEmpty()) { | |||
// No qualify profile computed on the project | |||
return; | |||
} | |||
Map<String, QualityProfile> rawProfiles = QPMeasureData.fromJson(rawMeasure.get().getStringValue()).getProfilesByKey(); | |||
Map<String, QualityProfile> baseProfiles = parseJsonData(baseMeasure.get()); | |||
detectNewOrUpdatedProfiles(baseProfiles, rawProfiles); | |||
detectNewOrUpdatedProfiles(baseProfiles, rawProfiles, branchComponent.getUuid()); | |||
detectNoMoreUsedProfiles(baseProfiles); | |||
} | |||
@@ -111,26 +119,39 @@ public class QualityProfileEventsStep implements ComputationStep { | |||
} | |||
} | |||
private void detectNewOrUpdatedProfiles(Map<String, QualityProfile> baseProfiles, Map<String, QualityProfile> rawProfiles) { | |||
private void detectNewOrUpdatedProfiles(Map<String, QualityProfile> baseProfiles, Map<String, QualityProfile> rawProfiles, String componentUuid) { | |||
for (QualityProfile profile : rawProfiles.values()) { | |||
qProfileStatusRepository.get(profile.getQpKey()).ifPresent(status -> { | |||
if (status.equals(ADDED)) { | |||
markAsAdded(profile); | |||
} else if (status.equals(UPDATED)) { | |||
markAsChanged(baseProfiles.get(profile.getQpKey()), profile); | |||
markAsChanged(baseProfiles.get(profile.getQpKey()), profile, componentUuid); | |||
} | |||
}); | |||
} | |||
} | |||
private void markAsChanged(QualityProfile baseProfile, QualityProfile profile) { | |||
Date from = baseProfile.getRulesUpdatedAt(); | |||
private void markAsChanged(QualityProfile baseProfile, QualityProfile profile, String componentUuid) { | |||
try { | |||
Map<ActiveRuleChange.Type, Long> changesMappedToNumberOfRules = qualityProfileRuleChangeTextResolver.mapChangeToNumberOfRules(baseProfile, componentUuid); | |||
String data = KeyValueFormat.format(ImmutableSortedMap.of( | |||
"key", profile.getQpKey(), | |||
"from", UtcDateUtils.formatDateTime(fixDate(from)), | |||
"to", UtcDateUtils.formatDateTime(fixDate(profile.getRulesUpdatedAt())))); | |||
eventRepository.add(createQProfileEvent(profile, "Changes in %s", data)); | |||
if (changesMappedToNumberOfRules.isEmpty()) { | |||
LOG.debug("No changes found for Quality Profile {}. Quality Profile event skipped.", profile.getQpKey()); | |||
return; | |||
} | |||
String data = KeyValueFormat.format(ImmutableSortedMap.of( | |||
"key", profile.getQpKey(), | |||
"from", UtcDateUtils.formatDateTime(fixDate(baseProfile.getRulesUpdatedAt())), | |||
"to", UtcDateUtils.formatDateTime(fixDate(profile.getRulesUpdatedAt())), | |||
"name", profile.getQpName(), | |||
"languageKey", profile.getLanguageKey())); | |||
String ruleChangeText = QualityProfileTextGenerator.generateRuleChangeText(changesMappedToNumberOfRules); | |||
eventRepository.add(createQProfileEvent(profile, "%s updated with " + ruleChangeText, data, ruleChangeText)); | |||
} catch (Exception e) { | |||
LOG.error("Failed to generate 'change' event for Quality Profile " + profile.getQpKey(), e); | |||
} | |||
} | |||
private void markAsRemoved(QualityProfile profile) { | |||
@@ -149,10 +170,14 @@ public class QualityProfileEventsStep implements ComputationStep { | |||
return Event.createProfile(String.format(namePattern, profileLabel(profile)), data, null); | |||
} | |||
private Event createQProfileEvent(QualityProfile profile, String namePattern, @Nullable String data, @Nullable String description) { | |||
return Event.createProfile(String.format(namePattern, profileLabel(profile)), data, description); | |||
} | |||
private String profileLabel(QualityProfile profile) { | |||
Optional<Language> language = languageRepository.find(profile.getLanguageKey()); | |||
String languageName = language.isPresent() ? language.get().getName() : profile.getLanguageKey(); | |||
return String.format("'%s' (%s)", profile.getQpName(), languageName); | |||
return String.format("\"%s\" (%s)", profile.getQpName(), languageName); | |||
} | |||
/** |
@@ -0,0 +1,296 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.ce.task.projectanalysis.qualityprofile; | |||
import java.util.Arrays; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.junit.runner.RunWith; | |||
import org.junit.runners.Parameterized; | |||
import org.junit.runners.Suite; | |||
import org.sonar.api.rules.CleanCodeAttribute; | |||
import org.sonar.api.utils.KeyValueFormat; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.component.SnapshotDao; | |||
import org.sonar.db.component.SnapshotDto; | |||
import org.sonar.db.qualityprofile.QProfileChangeDao; | |||
import org.sonar.db.qualityprofile.QProfileChangeDto; | |||
import org.sonar.db.qualityprofile.QProfileChangeQuery; | |||
import org.sonar.db.rule.RuleChangeDto; | |||
import org.sonar.server.qualityprofile.ActiveRuleChange; | |||
import org.sonar.server.qualityprofile.QualityProfile; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.ArgumentMatchers.eq; | |||
import static org.mockito.Mockito.doReturn; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED; | |||
import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.DEACTIVATED; | |||
import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.UPDATED; | |||
@RunWith(Suite.class) | |||
@Suite.SuiteClasses({ | |||
QualityProfileRuleChangeResolverTest.TextResolutionTest.class, | |||
QualityProfileRuleChangeResolverTest.ExceptionTest.class | |||
}) | |||
public class QualityProfileRuleChangeResolverTest { | |||
private final static String COMPONENT_UUID = "123"; | |||
@RunWith(Parameterized.class) | |||
public static class TextResolutionTest { | |||
private final DbClient dbClient = mock(DbClient.class); | |||
private final DbSession dbSession = mock(DbSession.class); | |||
private final QProfileChangeDao qProfileChangeDao = mock(QProfileChangeDao.class); | |||
private final SnapshotDao snapshotDao = mock(SnapshotDao.class); | |||
private final QualityProfile qualityProfile = mock(QualityProfile.class); | |||
private final QualityProfileRuleChangeResolver underTest = new QualityProfileRuleChangeResolver(dbClient); | |||
private final List<QProfileChangeDto> changes; | |||
private final Map<ActiveRuleChange.Type, Long> expectedMap; | |||
public TextResolutionTest(List<QProfileChangeDto> changes, Map<ActiveRuleChange.Type, Long> expectedMap) { | |||
this.changes = changes; | |||
this.expectedMap = expectedMap; | |||
} | |||
@Parameterized.Parameters | |||
public static Collection<Object[]> data() { | |||
return Arrays.asList(new Object[][]{ | |||
{ | |||
List.of( | |||
createChange(ACTIVATED, "ruleUuid1", 123L), | |||
createChange(DEACTIVATED, "ruleUuid2", 124L), | |||
createChange(UPDATED, "ruleUuid3", 125L) | |||
), | |||
Map.ofEntries( | |||
Map.entry(ACTIVATED, 1L), | |||
Map.entry(DEACTIVATED, 1L), | |||
Map.entry(UPDATED, 1L) | |||
) | |||
}, | |||
{ | |||
List.of( | |||
createChange(ACTIVATED, "ruleUuid1", 123L), | |||
createChange(DEACTIVATED, "ruleUuid1", 124L), // should cancel previous change | |||
createChange(UPDATED, "ruleUuid2", 125L) | |||
), | |||
Map.ofEntries( | |||
Map.entry(UPDATED, 1L) | |||
) | |||
}, | |||
{ | |||
List.of( | |||
createChange(ACTIVATED, "ruleUuid1", 123L), | |||
createChange(DEACTIVATED, "ruleUuid1", 124L), // should cancel previous change | |||
createChange(ACTIVATED, "ruleUuid1", 125L), | |||
createChange(UPDATED, "ruleUuid2", 126L) | |||
), | |||
Map.ofEntries( | |||
Map.entry(ACTIVATED, 1L), | |||
Map.entry(UPDATED, 1L) | |||
) | |||
}, | |||
{ | |||
List.of( | |||
createChange(ACTIVATED, "ruleUuid1", 123L), | |||
createCCTUpdate("ruleUuid1", 130L), // should overwrite previous change | |||
createChange(DEACTIVATED, "ruleUuid2", 124L), | |||
createChange(DEACTIVATED, "ruleUuid3", 125L), | |||
createChange(UPDATED, "ruleUuid4", 126L) | |||
), | |||
Map.ofEntries( | |||
Map.entry(DEACTIVATED, 2L), | |||
Map.entry(UPDATED, 2L) | |||
) | |||
}, | |||
{ | |||
List.of( | |||
createChange(ACTIVATED, "ruleUuid1", 123L), | |||
createChange(UPDATED, "ruleUuid1", 130L), | |||
createChange(DEACTIVATED, "ruleUuid1", 131L), // should overwrite update and cancel out the activation resulting to no change | |||
createCCTUpdate("ruleUuid2", 126L) | |||
), | |||
Map.ofEntries( | |||
Map.entry(UPDATED, 1L) | |||
) | |||
}, | |||
{ | |||
List.of( | |||
createChange(DEACTIVATED, "ruleUuid1", 123L), | |||
createChange(UPDATED, "ruleUuid1", 130L), | |||
createChange(UPDATED, "ruleUuid2", 126L) | |||
), | |||
Map.ofEntries( | |||
Map.entry(UPDATED, 2L) | |||
) | |||
}, | |||
{ | |||
// single CCT change | |||
List.of( | |||
createCCTUpdate("ruleUuid1", 123L) | |||
), | |||
Map.ofEntries( | |||
Map.entry(UPDATED, 1L) | |||
) | |||
}, | |||
{ | |||
// multiple CCT changes | |||
List.of( | |||
createCCTUpdate("ruleUuid1", 123L), | |||
createCCTUpdate("ruleUuid2", 124L) | |||
), | |||
Map.ofEntries( | |||
Map.entry(UPDATED, 2L) | |||
) | |||
}, | |||
{ | |||
// mixed CCT and old taxonomy changes | |||
List.of( | |||
createCCTUpdate("ruleUuid1", 123L), | |||
createChange(ACTIVATED, "ruleUuid2", 124L), | |||
createCCTUpdate("ruleUuid3", 125L), | |||
createChange(DEACTIVATED, "ruleUuid4", 126L), | |||
createChange(UPDATED, "ruleUuid3", 127L) | |||
), | |||
Map.ofEntries( | |||
Map.entry(ACTIVATED, 1L), | |||
Map.entry(DEACTIVATED, 1L), | |||
Map.entry(UPDATED, 2L) | |||
) | |||
}, | |||
{ | |||
List.of( | |||
createChange(ACTIVATED, "ruleUuid1", 123L), | |||
createChange(DEACTIVATED, "ruleUuid1", 124L) // should cancel previous change | |||
), | |||
Map.ofEntries() | |||
} | |||
}); | |||
} | |||
@Before | |||
public void setUp() { | |||
when(dbClient.openSession(false)).thenReturn(dbSession); | |||
doReturn(qProfileChangeDao).when(dbClient).qProfileChangeDao(); | |||
SnapshotDto snapshotDto = new SnapshotDto() | |||
.setAnalysisDate(123L); | |||
doReturn(Optional.of(snapshotDto)).when(snapshotDao).selectLastAnalysisByComponentUuid(dbSession, COMPONENT_UUID); | |||
doReturn(snapshotDao).when(dbClient).snapshotDao(); | |||
doReturn("profileUuid").when(qualityProfile).getQpKey(); | |||
doReturn("profileName").when(qualityProfile).getQpName(); | |||
} | |||
@Test | |||
public void givenQPChanges_whenResolveText_thenResolvedTextContainsAll() { | |||
// given | |||
doReturn(changes).when(qProfileChangeDao).selectByQuery(eq(dbSession), any(QProfileChangeQuery.class)); | |||
// when | |||
Map<ActiveRuleChange.Type, Long> changeToNumberOfRules = underTest.mapChangeToNumberOfRules(qualityProfile, COMPONENT_UUID); | |||
// then | |||
assertThat(changeToNumberOfRules).isEqualTo(expectedMap); | |||
} | |||
} | |||
public static class ExceptionTest { | |||
private final DbClient dbClient = mock(DbClient.class); | |||
private final DbSession dbSession = mock(DbSession.class); | |||
private final QProfileChangeDao qProfileChangeDao = mock(QProfileChangeDao.class); | |||
private final SnapshotDao snapshotDao = mock(SnapshotDao.class); | |||
private final QualityProfile qualityProfile = mock(QualityProfile.class); | |||
private final QualityProfileRuleChangeResolver underTest = new QualityProfileRuleChangeResolver(dbClient); | |||
@Before | |||
public void setUp() { | |||
when(dbClient.openSession(false)).thenReturn(dbSession); | |||
doReturn(qProfileChangeDao).when(dbClient).qProfileChangeDao(); | |||
SnapshotDto snapshotDto = new SnapshotDto() | |||
.setAnalysisDate(123L); | |||
doReturn(Optional.of(snapshotDto)).when(snapshotDao).selectLastAnalysisByComponentUuid(dbSession, COMPONENT_UUID); | |||
doReturn(snapshotDao).when(dbClient).snapshotDao(); | |||
doReturn("profileUuid").when(qualityProfile).getQpKey(); | |||
doReturn("profileName").when(qualityProfile).getQpName(); | |||
} | |||
@Test | |||
public void givenNoQPChanges_whenResolveText_thenThrows() { | |||
// given | |||
doReturn(List.of()).when(qProfileChangeDao).selectByQuery(eq(dbSession), any(QProfileChangeQuery.class)); | |||
// when then | |||
assertThatThrownBy(() -> underTest.mapChangeToNumberOfRules(qualityProfile, COMPONENT_UUID)) | |||
.isInstanceOf(IllegalStateException.class) | |||
.hasMessage("No profile changes found for profileName"); | |||
} | |||
@Test | |||
public void givenNoSnapshotFound_whenResolveText_thenThrows() { | |||
// given | |||
doReturn(Optional.empty()).when(snapshotDao).selectLastAnalysisByComponentUuid(dbSession, COMPONENT_UUID); | |||
// when then | |||
assertThatThrownBy(() -> underTest.mapChangeToNumberOfRules(qualityProfile, COMPONENT_UUID)) | |||
.isInstanceOf(IllegalStateException.class) | |||
.hasMessage("No snapshot found for 123"); | |||
} | |||
} | |||
private static QProfileChangeDto createChange(ActiveRuleChange.Type type, String ruleUuid, Long createdAt) { | |||
return new QProfileChangeDto() | |||
.setUuid("uuid") | |||
.setCreatedAt(createdAt) | |||
.setRulesProfileUuid("ruleProfileUuid") | |||
.setChangeType(type.name()) | |||
.setData(KeyValueFormat.parse("ruleUuid=" + ruleUuid)); | |||
} | |||
private static QProfileChangeDto createCCTUpdate(String ruleUuid, Long createdAt) { | |||
RuleChangeDto ruleChangeDto = new RuleChangeDto(); | |||
ruleChangeDto.setOldCleanCodeAttribute(CleanCodeAttribute.CONVENTIONAL); | |||
ruleChangeDto.setNewCleanCodeAttribute(CleanCodeAttribute.CLEAR); | |||
ruleChangeDto.setRuleUuid(ruleUuid); | |||
return new QProfileChangeDto() | |||
.setUuid("uuid") | |||
.setCreatedAt(createdAt) | |||
.setRulesProfileUuid("ruleProfileUuid") | |||
.setChangeType(UPDATED.name()) | |||
.setRuleChange(ruleChangeDto); | |||
} | |||
} |
@@ -0,0 +1,101 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.ce.task.projectanalysis.qualityprofile; | |||
import java.util.Arrays; | |||
import java.util.Collection; | |||
import java.util.Map; | |||
import org.junit.Test; | |||
import org.junit.runner.RunWith; | |||
import org.junit.runners.Parameterized; | |||
import org.sonar.server.qualityprofile.ActiveRuleChange; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
@RunWith(Parameterized.class) | |||
public class QualityProfileTextGeneratorTest { | |||
private final Map<ActiveRuleChange.Type, Long> changeToNumberOfRules; | |||
private final String expectedText; | |||
public QualityProfileTextGeneratorTest(Map<ActiveRuleChange.Type, Long> changeToNumberOfRules, String expectedText) { | |||
this.changeToNumberOfRules = changeToNumberOfRules; | |||
this.expectedText = expectedText; | |||
} | |||
@Parameterized.Parameters | |||
public static Collection<Object[]> data() { | |||
return Arrays.asList(new Object[][]{ | |||
{ | |||
Map.ofEntries( | |||
Map.entry(ActiveRuleChange.Type.ACTIVATED, 12L), | |||
Map.entry(ActiveRuleChange.Type.DEACTIVATED, 8L), | |||
Map.entry(ActiveRuleChange.Type.UPDATED, 5L) | |||
), | |||
"12 new rules, 8 deactivated rules and 5 modified rules" | |||
}, | |||
{ | |||
Map.ofEntries( | |||
Map.entry(ActiveRuleChange.Type.ACTIVATED, 1L), | |||
Map.entry(ActiveRuleChange.Type.DEACTIVATED, 0L), | |||
Map.entry(ActiveRuleChange.Type.UPDATED, 0L) | |||
), | |||
"1 new rule" | |||
}, | |||
{ | |||
Map.ofEntries( | |||
Map.entry(ActiveRuleChange.Type.DEACTIVATED, 5L) | |||
), | |||
"5 deactivated rules" | |||
}, | |||
{ | |||
Map.ofEntries( | |||
Map.entry(ActiveRuleChange.Type.UPDATED, 7L) | |||
), | |||
"7 modified rules" | |||
}, | |||
{ | |||
Map.ofEntries( | |||
Map.entry(ActiveRuleChange.Type.ACTIVATED, 1L), | |||
Map.entry(ActiveRuleChange.Type.DEACTIVATED, 1L), | |||
Map.entry(ActiveRuleChange.Type.UPDATED, 1L) | |||
), | |||
"1 new rule, 1 deactivated rule and 1 modified rule" | |||
}, | |||
{ | |||
Map.ofEntries( | |||
Map.entry(ActiveRuleChange.Type.ACTIVATED, 1L), | |||
Map.entry(ActiveRuleChange.Type.UPDATED, 3L) | |||
), | |||
"1 new rule and 3 modified rules" | |||
} | |||
}); | |||
} | |||
@Test | |||
public void givenRulesChanges_whenBuild_thenTextContainsAll() { | |||
// given when | |||
String updateMessage = QualityProfileTextGenerator.generateRuleChangeText(changeToNumberOfRules); | |||
// then | |||
assertThat(updateMessage).isEqualTo(expectedText); | |||
} | |||
} |
@@ -24,6 +24,7 @@ import java.util.Collections; | |||
import java.util.Date; | |||
import java.util.HashSet; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import javax.annotation.Nullable; | |||
@@ -31,9 +32,11 @@ import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.mockito.ArgumentCaptor; | |||
import org.slf4j.event.Level; | |||
import org.sonar.api.measures.CoreMetrics; | |||
import org.sonar.api.resources.AbstractLanguage; | |||
import org.sonar.api.resources.Language; | |||
import org.sonar.api.testfixtures.log.LogTester; | |||
import org.sonar.ce.task.projectanalysis.component.Component; | |||
import org.sonar.ce.task.projectanalysis.component.ReportComponent; | |||
import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule; | |||
@@ -46,8 +49,10 @@ import org.sonar.ce.task.projectanalysis.metric.Metric; | |||
import org.sonar.ce.task.projectanalysis.metric.MetricRepository; | |||
import org.sonar.ce.task.projectanalysis.qualityprofile.MutableQProfileStatusRepository; | |||
import org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepositoryImpl; | |||
import org.sonar.ce.task.projectanalysis.qualityprofile.QualityProfileRuleChangeResolver; | |||
import org.sonar.ce.task.step.TestComputationStepContext; | |||
import org.sonar.core.util.UtcDateUtils; | |||
import org.sonar.server.qualityprofile.ActiveRuleChange; | |||
import org.sonar.server.qualityprofile.QPMeasureData; | |||
import org.sonar.server.qualityprofile.QualityProfile; | |||
@@ -67,8 +72,20 @@ import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRep | |||
import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository.Status.UPDATED; | |||
public class QualityProfileEventsStepTest { | |||
private static final Date BEFORE_DATE = parseDateTime("2011-04-25T01:05:13+0100"); | |||
private static final Date AFTER_DATE = parseDateTime("2011-04-25T01:05:17+0100"); | |||
private static final Date BEFORE_DATE_PLUS_1_SEC = parseDateTime("2011-04-25T01:05:14+0100"); | |||
private static final Date AFTER_DATE_PLUS_1_SEC = parseDateTime("2011-04-25T01:05:18+0100"); | |||
private static final String RULE_CHANGE_TEXT = "1 new rule, 2 deactivated rules and 3 modified rules"; | |||
private static final Map<ActiveRuleChange.Type, Long> CHANGE_TO_NUMBER_OF_RULES_MAP = Map.ofEntries( | |||
Map.entry(ActiveRuleChange.Type.ACTIVATED, 1L), | |||
Map.entry(ActiveRuleChange.Type.DEACTIVATED, 2L), | |||
Map.entry(ActiveRuleChange.Type.UPDATED, 3L) | |||
); | |||
@Rule | |||
public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule(); | |||
@Rule | |||
public LogTester logTester = new LogTester(); | |||
private static final String QP_NAME_1 = "qp_1"; | |||
private static final String QP_NAME_2 = "qp_2"; | |||
@@ -76,16 +93,16 @@ public class QualityProfileEventsStepTest { | |||
private static final String LANGUAGE_KEY_2 = "language_key_2"; | |||
private static final String LANGUAGE_KEY_3 = "languageKey3"; | |||
private MetricRepository metricRepository = mock(MetricRepository.class); | |||
private MeasureRepository measureRepository = mock(MeasureRepository.class); | |||
private LanguageRepository languageRepository = mock(LanguageRepository.class); | |||
private EventRepository eventRepository = mock(EventRepository.class); | |||
private ArgumentCaptor<Event> eventArgumentCaptor = ArgumentCaptor.forClass(Event.class); | |||
private MutableQProfileStatusRepository qProfileStatusRepository = new QProfileStatusRepositoryImpl(); | |||
private Metric qualityProfileMetric = mock(Metric.class); | |||
private final MetricRepository metricRepository = mock(MetricRepository.class); | |||
private final MeasureRepository measureRepository = mock(MeasureRepository.class); | |||
private final LanguageRepository languageRepository = mock(LanguageRepository.class); | |||
private final EventRepository eventRepository = mock(EventRepository.class); | |||
private final ArgumentCaptor<Event> eventArgumentCaptor = ArgumentCaptor.forClass(Event.class); | |||
private final MutableQProfileStatusRepository qProfileStatusRepository = new QProfileStatusRepositoryImpl(); | |||
private QualityProfileEventsStep underTest = new QualityProfileEventsStep(treeRootHolder, metricRepository, measureRepository, languageRepository, eventRepository, qProfileStatusRepository); | |||
private final Metric qualityProfileMetric = mock(Metric.class); | |||
private final QualityProfileRuleChangeResolver qualityProfileRuleChangeTextResolver = mock(QualityProfileRuleChangeResolver.class); | |||
private final QualityProfileEventsStep underTest = new QualityProfileEventsStep(treeRootHolder, metricRepository, measureRepository, languageRepository, eventRepository, qProfileStatusRepository, qualityProfileRuleChangeTextResolver); | |||
@Before | |||
public void setUp() { | |||
@@ -114,7 +131,7 @@ public class QualityProfileEventsStepTest { | |||
@Test | |||
public void no_event_if_no_base_and_quality_profile_measure_is_empty() { | |||
mockMeasures(treeRootHolder.getRoot(), null, null); | |||
mockQualityProfileMeasures(treeRootHolder.getRoot(), null, null); | |||
underTest.execute(new TestComputationStepContext()); | |||
@@ -127,13 +144,13 @@ public class QualityProfileEventsStepTest { | |||
qProfileStatusRepository.register(qp.getQpKey(), ADDED); | |||
Language language = mockLanguageInRepository(LANGUAGE_KEY_1); | |||
mockMeasures(treeRootHolder.getRoot(), null, arrayOf(qp)); | |||
mockQualityProfileMeasures(treeRootHolder.getRoot(), null, arrayOf(qp)); | |||
underTest.execute(new TestComputationStepContext()); | |||
verify(eventRepository).add(eventArgumentCaptor.capture()); | |||
verifyNoMoreInteractions(eventRepository); | |||
verifyEvent(eventArgumentCaptor.getValue(), "Use '" + qp.getQpName() + "' (" + language.getName() + ")", null); | |||
verifyEvent(eventArgumentCaptor.getValue(), "Use \"" + qp.getQpName() + "\" (" + language.getName() + ")", null, null); | |||
} | |||
@Test | |||
@@ -142,13 +159,13 @@ public class QualityProfileEventsStepTest { | |||
qProfileStatusRepository.register(qp.getQpKey(), ADDED); | |||
mockLanguageNotInRepository(LANGUAGE_KEY_1); | |||
mockMeasures(treeRootHolder.getRoot(), null, arrayOf(qp)); | |||
mockQualityProfileMeasures(treeRootHolder.getRoot(), null, arrayOf(qp)); | |||
underTest.execute(new TestComputationStepContext()); | |||
verify(eventRepository).add(eventArgumentCaptor.capture()); | |||
verifyNoMoreInteractions(eventRepository); | |||
verifyEvent(eventArgumentCaptor.getValue(), "Use '" + qp.getQpName() + "' (" + qp.getLanguageKey() + ")", null); | |||
verifyEvent(eventArgumentCaptor.getValue(), "Use \"" + qp.getQpName() + "\" (" + qp.getLanguageKey() + ")", null, null); | |||
} | |||
@Test | |||
@@ -156,35 +173,35 @@ public class QualityProfileEventsStepTest { | |||
QualityProfile qp = qp(QP_NAME_1, LANGUAGE_KEY_1, new Date()); | |||
qProfileStatusRepository.register(qp.getQpKey(), REMOVED); | |||
mockMeasures(treeRootHolder.getRoot(), arrayOf(qp), null); | |||
mockQualityProfileMeasures(treeRootHolder.getRoot(), arrayOf(qp), null); | |||
Language language = mockLanguageInRepository(LANGUAGE_KEY_1); | |||
underTest.execute(new TestComputationStepContext()); | |||
verify(eventRepository).add(eventArgumentCaptor.capture()); | |||
verifyNoMoreInteractions(eventRepository); | |||
verifyEvent(eventArgumentCaptor.getValue(), "Stop using '" + qp.getQpName() + "' (" + language.getName() + ")", null); | |||
verifyEvent(eventArgumentCaptor.getValue(), "Stop using \"" + qp.getQpName() + "\" (" + language.getName() + ")", null, null); | |||
} | |||
@Test | |||
public void no_more_used_event_uses_language_key_in_message_if_language_not_found() { | |||
QualityProfile qp = qp(QP_NAME_1, LANGUAGE_KEY_1, new Date()); | |||
qProfileStatusRepository.register(qp.getQpKey(), REMOVED); | |||
mockMeasures(treeRootHolder.getRoot(), arrayOf(qp), null); | |||
mockQualityProfileMeasures(treeRootHolder.getRoot(), arrayOf(qp), null); | |||
mockLanguageNotInRepository(LANGUAGE_KEY_1); | |||
underTest.execute(new TestComputationStepContext()); | |||
verify(eventRepository).add(eventArgumentCaptor.capture()); | |||
verifyNoMoreInteractions(eventRepository); | |||
verifyEvent(eventArgumentCaptor.getValue(), "Stop using '" + qp.getQpName() + "' (" + qp.getLanguageKey() + ")", null); | |||
verifyEvent(eventArgumentCaptor.getValue(), "Stop using \"" + qp.getQpName() + "\" (" + qp.getLanguageKey() + ")", null, null); | |||
} | |||
@Test | |||
public void no_event_if_qp_is_unchanged() { | |||
QualityProfile qp = qp(QP_NAME_1, LANGUAGE_KEY_1, new Date()); | |||
qProfileStatusRepository.register(qp.getQpKey(), UNCHANGED); | |||
mockMeasures(treeRootHolder.getRoot(), arrayOf(qp), arrayOf(qp)); | |||
mockQualityProfileMeasures(treeRootHolder.getRoot(), arrayOf(qp), arrayOf(qp)); | |||
underTest.execute(new TestComputationStepContext()); | |||
@@ -193,20 +210,97 @@ public class QualityProfileEventsStepTest { | |||
@Test | |||
public void changed_event_if_qp_has_been_updated() { | |||
QualityProfile qp1 = qp(QP_NAME_1, LANGUAGE_KEY_1, parseDateTime("2011-04-25T01:05:13+0100")); | |||
QualityProfile qp2 = qp(QP_NAME_1, LANGUAGE_KEY_1, parseDateTime("2011-04-25T01:05:17+0100")); | |||
QualityProfile qp1 = qp(QP_NAME_1, LANGUAGE_KEY_1, BEFORE_DATE); | |||
QualityProfile qp2 = qp(QP_NAME_1, LANGUAGE_KEY_1, AFTER_DATE); | |||
qProfileStatusRepository.register(qp2.getQpKey(), UPDATED); | |||
mockMeasures(treeRootHolder.getRoot(), arrayOf(qp1), arrayOf(qp2)); | |||
mockQualityProfileMeasures(treeRootHolder.getRoot(), arrayOf(qp1), arrayOf(qp2)); | |||
Language language = mockLanguageInRepository(LANGUAGE_KEY_1); | |||
when(qualityProfileRuleChangeTextResolver.mapChangeToNumberOfRules(qp2, treeRootHolder.getRoot().getUuid())).thenReturn(CHANGE_TO_NUMBER_OF_RULES_MAP); | |||
underTest.execute(new TestComputationStepContext()); | |||
verify(eventRepository).add(eventArgumentCaptor.capture()); | |||
verifyNoMoreInteractions(eventRepository); | |||
verifyEvent(eventArgumentCaptor.getValue(), | |||
"Changes in '" + qp2.getQpName() + "' (" + language.getName() + ")", | |||
"from=" + UtcDateUtils.formatDateTime(parseDateTime("2011-04-25T01:05:14+0100")) + ";key=" + qp1.getQpKey() + ";to=" | |||
+ UtcDateUtils.formatDateTime(parseDateTime("2011-04-25T01:05:18+0100"))); | |||
"\"" + qp2.getQpName() + "\" (" + language.getName() + ") updated with " + RULE_CHANGE_TEXT, | |||
"from=" + UtcDateUtils.formatDateTime(BEFORE_DATE_PLUS_1_SEC) + | |||
";key=" + qp1.getQpKey() + | |||
";languageKey=" + qp2.getLanguageKey()+ | |||
";name=" + qp2.getQpName() + | |||
";to=" + UtcDateUtils.formatDateTime(AFTER_DATE_PLUS_1_SEC), | |||
RULE_CHANGE_TEXT); | |||
} | |||
@Test | |||
public void givenRulesWhereAddedModifiedOrRemoved_whenEventStep_thenQPChangeEventIsAddedWithDetails() { | |||
QualityProfile qp1 = qp(QP_NAME_1, LANGUAGE_KEY_1, BEFORE_DATE); | |||
QualityProfile qp2 = qp(QP_NAME_1, LANGUAGE_KEY_1, AFTER_DATE); | |||
// mock updated profile | |||
qProfileStatusRepository.register(qp2.getQpKey(), UPDATED); | |||
mockQualityProfileMeasures(treeRootHolder.getRoot(), arrayOf(qp1), arrayOf(qp2)); | |||
Language language = mockLanguageInRepository(LANGUAGE_KEY_1); | |||
// mock rule changes | |||
when(qualityProfileRuleChangeTextResolver.mapChangeToNumberOfRules(qp2, treeRootHolder.getRoot().getUuid())).thenReturn(CHANGE_TO_NUMBER_OF_RULES_MAP); | |||
underTest.execute(new TestComputationStepContext()); | |||
verify(eventRepository).add(eventArgumentCaptor.capture()); | |||
verifyNoMoreInteractions(eventRepository); | |||
verifyEvent(eventArgumentCaptor.getValue(), | |||
"\"" + qp2.getQpName() + "\" (" + language.getName() + ") updated with " + RULE_CHANGE_TEXT, | |||
"from=" + UtcDateUtils.formatDateTime(BEFORE_DATE_PLUS_1_SEC) + | |||
";key=" + qp1.getQpKey() + | |||
";languageKey=" + qp2.getLanguageKey()+ | |||
";name=" + qp2.getQpName() + | |||
";to=" + UtcDateUtils.formatDateTime(AFTER_DATE_PLUS_1_SEC), | |||
RULE_CHANGE_TEXT); | |||
} | |||
@Test | |||
public void givenRuleTextResolverException_whenEventStep_thenLogAndContinue() { | |||
// given | |||
logTester.setLevel(Level.ERROR); | |||
QualityProfile existingQP = qp(QP_NAME_1, LANGUAGE_KEY_1, BEFORE_DATE); | |||
QualityProfile newQP = qp(QP_NAME_1, LANGUAGE_KEY_1, AFTER_DATE); | |||
// mock updated profile | |||
qProfileStatusRepository.register(newQP.getQpKey(), UPDATED); | |||
mockQualityProfileMeasures(treeRootHolder.getRoot(), arrayOf(existingQP), arrayOf(newQP)); | |||
when(qualityProfileRuleChangeTextResolver.mapChangeToNumberOfRules(newQP, treeRootHolder.getRoot().getUuid())).thenThrow(new RuntimeException("error")); | |||
var context = new TestComputationStepContext(); | |||
// when | |||
underTest.execute(context); | |||
// then | |||
assertThat(logTester.logs(Level.ERROR)).containsExactly("Failed to generate 'change' event for Quality Profile " + newQP.getQpKey()); | |||
verify(eventRepository, never()).add(any(Event.class)); | |||
} | |||
@Test | |||
public void givenNoChangesFound_whenEventStep_thenDebugLogAndSkipEvent() { | |||
// given | |||
logTester.setLevel(Level.DEBUG); | |||
QualityProfile existingQP = qp(QP_NAME_1, LANGUAGE_KEY_1, BEFORE_DATE); | |||
QualityProfile newQP = qp(QP_NAME_1, LANGUAGE_KEY_1, AFTER_DATE); | |||
// mock updated profile | |||
qProfileStatusRepository.register(newQP.getQpKey(), UPDATED); | |||
mockQualityProfileMeasures(treeRootHolder.getRoot(), arrayOf(existingQP), arrayOf(newQP)); | |||
when(qualityProfileRuleChangeTextResolver.mapChangeToNumberOfRules(newQP, treeRootHolder.getRoot().getUuid())).thenReturn(Collections.emptyMap()); | |||
var context = new TestComputationStepContext(); | |||
// when | |||
underTest.execute(context); | |||
// then | |||
assertThat(logTester.logs(Level.DEBUG)).containsExactly("No changes found for Quality Profile " + newQP.getQpKey() + ". Quality Profile event skipped."); | |||
verify(eventRepository, never()).add(any(Event.class)); | |||
} | |||
@Test | |||
@@ -220,11 +314,11 @@ public class QualityProfileEventsStepTest { | |||
Date date = new Date(); | |||
QualityProfile qp1 = qp(QP_NAME_2, LANGUAGE_KEY_1, date); | |||
QualityProfile qp2 = qp(QP_NAME_2, LANGUAGE_KEY_2, date); | |||
QualityProfile qp3 = qp(QP_NAME_1, LANGUAGE_KEY_1, parseDateTime("2011-04-25T01:05:13+0100")); | |||
QualityProfile qp3_updated = qp(QP_NAME_1, LANGUAGE_KEY_1, parseDateTime("2011-04-25T01:05:17+0100")); | |||
QualityProfile qp3 = qp(QP_NAME_1, LANGUAGE_KEY_1, BEFORE_DATE); | |||
QualityProfile qp3_updated = qp(QP_NAME_1, LANGUAGE_KEY_1, AFTER_DATE); | |||
QualityProfile qp4 = qp(QP_NAME_2, LANGUAGE_KEY_3, date); | |||
mockMeasures( | |||
mockQualityProfileMeasures( | |||
treeRootHolder.getRoot(), | |||
arrayOf(qp1, qp2, qp3), | |||
arrayOf(qp3_updated, qp2, qp4)); | |||
@@ -234,12 +328,14 @@ public class QualityProfileEventsStepTest { | |||
qProfileStatusRepository.register(qp3.getQpKey(), UPDATED); | |||
qProfileStatusRepository.register(qp4.getQpKey(), ADDED); | |||
when(qualityProfileRuleChangeTextResolver.mapChangeToNumberOfRules(qp3_updated, treeRootHolder.getRoot().getUuid())).thenReturn(CHANGE_TO_NUMBER_OF_RULES_MAP); | |||
underTest.execute(new TestComputationStepContext()); | |||
assertThat(events).extracting("name").containsOnly( | |||
"Stop using '" + QP_NAME_2 + "' (" + LANGUAGE_KEY_1 + ")", | |||
"Use '" + QP_NAME_2 + "' (" + LANGUAGE_KEY_3 + ")", | |||
"Changes in '" + QP_NAME_1 + "' (" + LANGUAGE_KEY_1 + ")"); | |||
"Stop using \"" + QP_NAME_2 + "\" (" + LANGUAGE_KEY_1 + ")", | |||
"Use \"" + QP_NAME_2 + "\" (" + LANGUAGE_KEY_3 + ")", | |||
"\"" + QP_NAME_1 + "\" (" + LANGUAGE_KEY_1 + ") updated with " + RULE_CHANGE_TEXT); | |||
} | |||
private Language mockLanguageInRepository(String languageKey) { | |||
@@ -261,16 +357,16 @@ public class QualityProfileEventsStepTest { | |||
when(languageRepository.find(anyString())).thenReturn(Optional.empty()); | |||
} | |||
private void mockMeasures(Component component, @Nullable QualityProfile[] previous, @Nullable QualityProfile[] current) { | |||
private void mockQualityProfileMeasures(Component component, @Nullable QualityProfile[] previous, @Nullable QualityProfile[] current) { | |||
when(measureRepository.getBaseMeasure(component, qualityProfileMetric)).thenReturn(Optional.of(newMeasure(previous))); | |||
when(measureRepository.getRawMeasure(component, qualityProfileMetric)).thenReturn(Optional.of(newMeasure(current))); | |||
} | |||
private static void verifyEvent(Event event, String expectedName, @Nullable String expectedData) { | |||
private static void verifyEvent(Event event, String expectedName, @Nullable String expectedData, @Nullable String expectedDescription) { | |||
assertThat(event.getName()).isEqualTo(expectedName); | |||
assertThat(event.getData()).isEqualTo(expectedData); | |||
assertThat(event.getCategory()).isEqualTo(Event.Category.PROFILE); | |||
assertThat(event.getDescription()).isNull(); | |||
assertThat(event.getDescription()).isEqualTo(expectedDescription); | |||
} | |||
private static QualityProfile qp(String qpName, String languageKey, Date date) { |