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;
IssueFilter.class,
FlowGenerator.class,
+ QualityProfileRuleChangeResolver.class,
// push events
PushEventFactory.class,
--- /dev/null
+/*
+ * 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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));
+ };
+ }
+}
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;
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;
* 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
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);
}
}
}
- 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) {
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);
}
/**
--- /dev/null
+/*
+ * 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);
+ }
+
+}
\ No newline at end of file
--- /dev/null
+/*
+ * 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);
+ }
+
+}
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;
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;
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;
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";
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() {
@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());
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
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
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());
@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
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));
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) {
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) {