From 1bb396e6e43ae65bc78cf19e7ca5c0b538328920 Mon Sep 17 00:00:00 2001 From: Dimitris Kavvathas Date: Thu, 12 Oct 2023 10:07:37 +0200 Subject: [PATCH] SONAR-20665 Add exact rule changes (added, deactivated, modified) in Quality Profile event --- ...ProjectAnalysisTaskContainerPopulator.java | 2 + .../QualityProfileRuleChangeResolver.java | 139 ++++++++ .../QualityProfileTextGenerator.java | 74 +++++ .../step/QualityProfileEventsStep.java | 53 +++- .../QualityProfileRuleChangeResolverTest.java | 296 ++++++++++++++++++ .../QualityProfileTextGeneratorTest.java | 101 ++++++ .../step/QualityProfileEventsStepTest.java | 164 ++++++++-- 7 files changed, 781 insertions(+), 48 deletions(-) create mode 100644 server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileRuleChangeResolver.java create mode 100644 server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileTextGenerator.java create mode 100644 server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileRuleChangeResolverTest.java create mode 100644 server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileTextGeneratorTest.java diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java index c39754d47f8..78594ce7a5d 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java @@ -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, diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileRuleChangeResolver.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileRuleChangeResolver.java new file mode 100644 index 00000000000..32aadb00d24 --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileRuleChangeResolver.java @@ -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 mapChangeToNumberOfRules(QualityProfile profile, String componentUuid) { + // get profile changes + List profileChanges = getProfileChanges(profile, componentUuid); + + Map> updatedRulesGrouped = profileChanges.stream() + .filter(QualityProfileRuleChangeResolver::hasRuleUuid) + .collect(Collectors.groupingBy(p -> p.getRuleChange() != null ? p.getRuleChange().getRuleUuid() : p.getDataAsMap().get("ruleUuid"))); + + Map rulesMappedToFinalChange = getRulesMappedToFinalChange(updatedRulesGrouped); + return getChangeMappedToNumberOfRules(rulesMappedToFinalChange); + } + + @NotNull + private static Map getChangeMappedToNumberOfRules(Map 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 getRulesMappedToFinalChange(Map> updatedRulesGrouped) { + return updatedRulesGrouped.entrySet().stream() + .map(entry -> { + String key = entry.getKey(); + List ruleChanges = entry.getValue(); + + // get last change + QProfileChangeDto lastChange = ruleChanges.stream().max(Comparator.comparing(QProfileChangeDto::getCreatedAt)).orElseThrow(); + Optional 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 getProfileChanges(QualityProfile profile, String componentUuid) { + try (DbSession dbSession = dbClient.openSession(false)) { + QProfileChangeQuery query = new QProfileChangeQuery(profile.getQpKey()); + query.setFromIncluded(getLastAnalysisDate(componentUuid, dbSession)); + List 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(); + } + +} diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileTextGenerator.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileTextGenerator.java new file mode 100644 index 00000000000..b40c1cc584f --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileTextGenerator.java @@ -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 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 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, 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)); + }; + } +} diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/QualityProfileEventsStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/QualityProfileEventsStep.java index d80ab7addc5..22a849de831 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/QualityProfileEventsStep.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/QualityProfileEventsStep.java @@ -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 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 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 rawProfiles = QPMeasureData.fromJson(rawMeasure.get().getStringValue()).getProfilesByKey(); Map 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 baseProfiles, Map rawProfiles) { + private void detectNewOrUpdatedProfiles(Map baseProfiles, Map 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 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 = 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); } /** diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileRuleChangeResolverTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileRuleChangeResolverTest.java new file mode 100644 index 00000000000..64ac60cd32b --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileRuleChangeResolverTest.java @@ -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 changes; + private final Map expectedMap; + + + public TextResolutionTest(List changes, Map expectedMap) { + this.changes = changes; + this.expectedMap = expectedMap; + } + + @Parameterized.Parameters + public static Collection 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 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); + } + +} \ No newline at end of file diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileTextGeneratorTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileTextGeneratorTest.java new file mode 100644 index 00000000000..7884d8fae4f --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileTextGeneratorTest.java @@ -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 changeToNumberOfRules; + private final String expectedText; + + public QualityProfileTextGeneratorTest(Map changeToNumberOfRules, String expectedText) { + this.changeToNumberOfRules = changeToNumberOfRules; + this.expectedText = expectedText; + } + + @Parameterized.Parameters + public static Collection 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); + } + +} diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/QualityProfileEventsStepTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/QualityProfileEventsStepTest.java index 3ea123e8969..8811ce81caf 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/QualityProfileEventsStepTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/QualityProfileEventsStepTest.java @@ -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 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 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 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) { -- 2.39.5