Parcourir la source

SONAR-20665 Add exact rule changes (added, deactivated, modified) in Quality Profile event

tags/10.3.0.82913
Dimitris Kavvathas il y a 7 mois
Parent
révision
1bb396e6e4

+ 2
- 0
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java Voir le fichier

@@ -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,


+ 139
- 0
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileRuleChangeResolver.java Voir le fichier

@@ -0,0 +1,139 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.ce.task.projectanalysis.qualityprofile;

import java.util.AbstractMap;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.qualityprofile.QProfileChangeDto;
import org.sonar.db.qualityprofile.QProfileChangeQuery;
import org.sonar.server.qualityprofile.ActiveRuleChange;
import org.sonar.server.qualityprofile.QualityProfile;

import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED;
import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.DEACTIVATED;
import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.UPDATED;

public class QualityProfileRuleChangeResolver {
private final DbClient dbClient;

public QualityProfileRuleChangeResolver(DbClient dbClient) {
this.dbClient = dbClient;
}

/**
* Returns a text description of the changes made to a quality profile.
* The text is generated by looking at the last change for each rule and determining the final status of the rule.
* For old taxonomy, we need to access the QProfileChangeDto.data field to determine the ruleUuid.
* For CCT, we can use the QProfileChangeDto.ruleChange field to determine the ruleUuid.
*
* @param profile the quality profile to generate the text for
* @return a text description of the changes made to the profile
* @throws IllegalStateException if no changes are found for the profile
*/
public Map<ActiveRuleChange.Type, Long> mapChangeToNumberOfRules(QualityProfile profile, String componentUuid) {
// get profile changes
List<QProfileChangeDto> profileChanges = getProfileChanges(profile, componentUuid);

Map<String, List<QProfileChangeDto>> updatedRulesGrouped = profileChanges.stream()
.filter(QualityProfileRuleChangeResolver::hasRuleUuid)
.collect(Collectors.groupingBy(p -> p.getRuleChange() != null ? p.getRuleChange().getRuleUuid() : p.getDataAsMap().get("ruleUuid")));

Map<String, ActiveRuleChange.Type> rulesMappedToFinalChange = getRulesMappedToFinalChange(updatedRulesGrouped);
return getChangeMappedToNumberOfRules(rulesMappedToFinalChange);
}

@NotNull
private static Map<ActiveRuleChange.Type, Long> getChangeMappedToNumberOfRules(Map<String, ActiveRuleChange.Type> rulesMappedToFinalChange) {
return rulesMappedToFinalChange.values().stream()
.collect(Collectors.groupingBy(
actionType -> actionType,
Collectors.counting()
));
}

private static boolean hasRuleUuid(QProfileChangeDto change) {
return (change.getRuleChange() != null && change.getRuleChange().getRuleUuid() != null) ||
(!change.getDataAsMap().isEmpty() && change.getDataAsMap().containsKey("ruleUuid"));
}

/**
* Returns a map of ruleUuid to the final status of the rule.
* If the rule final status is the same as the initial status, the value will be empty.
*
* @param updatedRulesGrouped a map of ruleUuid to a list of changes for that rule
* @return a map of ruleUuid to the final status of the rule. If the rule final status is the same as the initial status, the value will be empty.
*/
private static Map<String, ActiveRuleChange.Type> getRulesMappedToFinalChange(Map<String, List<QProfileChangeDto>> updatedRulesGrouped) {
return updatedRulesGrouped.entrySet().stream()
.map(entry -> {
String key = entry.getKey();
List<QProfileChangeDto> ruleChanges = entry.getValue();

// get last change
QProfileChangeDto lastChange = ruleChanges.stream().max(Comparator.comparing(QProfileChangeDto::getCreatedAt)).orElseThrow();
Optional<ActiveRuleChange.Type> value;

if (UPDATED.name().equals(lastChange.getChangeType())) {
value = Optional.of(UPDATED);
} else {
// for ACTIVATED/DEACTIVATED we need to count the number of times the rule was toggled
long activationToggles = ruleChanges.stream()
.filter(rule -> List.of(ACTIVATED.name(), DEACTIVATED.name()).contains(rule.getChangeType()))
.count();
// If the count is even, skip all rules in this group as the status is unchanged
// If the count is odd we only care about the last status update
value = activationToggles % 2 == 0 ? Optional.empty() : Optional.of(ActiveRuleChange.Type.valueOf(lastChange.getChangeType()));
}

return new AbstractMap.SimpleEntry<>(key, value);
})
.filter(entry -> entry.getValue().isPresent())
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().get()
));
}

private List<QProfileChangeDto> getProfileChanges(QualityProfile profile, String componentUuid) {
try (DbSession dbSession = dbClient.openSession(false)) {
QProfileChangeQuery query = new QProfileChangeQuery(profile.getQpKey());
query.setFromIncluded(getLastAnalysisDate(componentUuid, dbSession));
List<QProfileChangeDto> profileChanges = dbClient.qProfileChangeDao().selectByQuery(dbSession, query);
if (profileChanges.isEmpty()) {
throw new IllegalStateException("No profile changes found for " + profile.getQpName());
}
return profileChanges;
}
}

@NotNull
private Long getLastAnalysisDate(String componentUuid, DbSession dbSession) {
return dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, componentUuid)
.orElseThrow(() -> new IllegalStateException("No snapshot found for " + componentUuid)).getAnalysisDate();
}

}

+ 74
- 0
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileTextGenerator.java Voir le fichier

@@ -0,0 +1,74 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.ce.task.projectanalysis.qualityprofile;

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.sonar.server.qualityprofile.ActiveRuleChange;

/**
* Builder for generating a text description of the changes made to a quality profile.
*/
public final class QualityProfileTextGenerator {

private static final Map<ActiveRuleChange.Type, String> CHANGE_TO_TEXT_MAP = Map.ofEntries(
Map.entry(ActiveRuleChange.Type.ACTIVATED, " new rule"),
Map.entry(ActiveRuleChange.Type.DEACTIVATED, " deactivated rule"),
Map.entry(ActiveRuleChange.Type.UPDATED, " modified rule")
);

private QualityProfileTextGenerator() {
// only static methods
}

/**
* Returns a text description of the changes made to a quality profile. Oxford comma is not used.
* The order of the changes is based on the order of the enum name (activated, deactivated, updated) to keep consistency.
* 0 values are filtered out.
*
* @param changesMappedToNumberOfRules the changes mapped to the number of rules
* @return a text description of the changes made to the profile
*/
public static String generateRuleChangeText(Map<ActiveRuleChange.Type, Long> changesMappedToNumberOfRules) {

return changesMappedToNumberOfRules.entrySet().stream()
.sorted(Map.Entry.comparingByKey(Comparator.comparing(Enum::name)))
.filter(entry -> entry.getValue() > 0)
.map(entry -> generateRuleText(entry.getValue(), CHANGE_TO_TEXT_MAP.get(entry.getKey())))
.collect(Collectors.collectingAndThen(Collectors.toList(), joiningLastDelimiter(", ", " and ")));
}

private static String generateRuleText(Long ruleNumber, String ruleText) {
return ruleNumber + ruleText + (ruleNumber > 1 ? "s" : "");
}

private static Function<List<String>, String> joiningLastDelimiter(String delimiter, String lastDelimiter) {
return list -> {
int last = list.size() - 1;
if (last < 1) return String.join(delimiter, list);
return String.join(lastDelimiter,
String.join(delimiter, list.subList(0, last)),
list.get(last));
};
}
}

+ 39
- 14
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/QualityProfileEventsStep.java Voir le fichier

@@ -26,6 +26,8 @@ import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
import org.apache.commons.lang.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.resources.Language;
import org.sonar.api.utils.KeyValueFormat;
@@ -38,8 +40,11 @@ import org.sonar.ce.task.projectanalysis.measure.Measure;
import org.sonar.ce.task.projectanalysis.measure.MeasureRepository;
import org.sonar.ce.task.projectanalysis.metric.MetricRepository;
import org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository;
import org.sonar.ce.task.projectanalysis.qualityprofile.QualityProfileRuleChangeResolver;
import org.sonar.ce.task.projectanalysis.qualityprofile.QualityProfileTextGenerator;
import org.sonar.ce.task.step.ComputationStep;
import org.sonar.core.util.UtcDateUtils;
import org.sonar.server.qualityprofile.ActiveRuleChange;
import org.sonar.server.qualityprofile.QPMeasureData;
import org.sonar.server.qualityprofile.QualityProfile;

@@ -52,22 +57,25 @@ import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRep
* As it depends upon {@link CoreMetrics#QUALITY_PROFILES_KEY}, it must be executed after {@link ComputeQProfileMeasureStep}
*/
public class QualityProfileEventsStep implements ComputationStep {
private static final Logger LOG = LoggerFactory.getLogger(QualityProfileEventsStep.class);
private final TreeRootHolder treeRootHolder;
private final MetricRepository metricRepository;
private final MeasureRepository measureRepository;
private final EventRepository eventRepository;
private final LanguageRepository languageRepository;
private final QProfileStatusRepository qProfileStatusRepository;
private final QualityProfileRuleChangeResolver qualityProfileRuleChangeTextResolver;

public QualityProfileEventsStep(TreeRootHolder treeRootHolder,
MetricRepository metricRepository, MeasureRepository measureRepository, LanguageRepository languageRepository,
EventRepository eventRepository, QProfileStatusRepository qProfileStatusRepository) {
EventRepository eventRepository, QProfileStatusRepository qProfileStatusRepository, QualityProfileRuleChangeResolver qualityProfileRuleChangeTextResolver) {
this.treeRootHolder = treeRootHolder;
this.metricRepository = metricRepository;
this.measureRepository = measureRepository;
this.eventRepository = eventRepository;
this.languageRepository = languageRepository;
this.qProfileStatusRepository = qProfileStatusRepository;
this.qualityProfileRuleChangeTextResolver = qualityProfileRuleChangeTextResolver;
}

@Override
@@ -77,21 +85,21 @@ public class QualityProfileEventsStep implements ComputationStep {

private void executeForBranch(Component branchComponent) {
Optional<Measure> baseMeasure = measureRepository.getBaseMeasure(branchComponent, metricRepository.getByKey(CoreMetrics.QUALITY_PROFILES_KEY));
if (!baseMeasure.isPresent()) {
if (baseMeasure.isEmpty()) {
// first analysis -> do not generate events
return;
}

// Load profiles used in current analysis for which at least one file of the corresponding language exists
Optional<Measure> rawMeasure = measureRepository.getRawMeasure(branchComponent, metricRepository.getByKey(CoreMetrics.QUALITY_PROFILES_KEY));
if (!rawMeasure.isPresent()) {
if (rawMeasure.isEmpty()) {
// No qualify profile computed on the project
return;
}
Map<String, QualityProfile> rawProfiles = QPMeasureData.fromJson(rawMeasure.get().getStringValue()).getProfilesByKey();

Map<String, QualityProfile> baseProfiles = parseJsonData(baseMeasure.get());
detectNewOrUpdatedProfiles(baseProfiles, rawProfiles);
detectNewOrUpdatedProfiles(baseProfiles, rawProfiles, branchComponent.getUuid());
detectNoMoreUsedProfiles(baseProfiles);
}

@@ -111,26 +119,39 @@ public class QualityProfileEventsStep implements ComputationStep {
}
}

private void detectNewOrUpdatedProfiles(Map<String, QualityProfile> baseProfiles, Map<String, QualityProfile> rawProfiles) {
private void detectNewOrUpdatedProfiles(Map<String, QualityProfile> baseProfiles, Map<String, QualityProfile> rawProfiles, String componentUuid) {
for (QualityProfile profile : rawProfiles.values()) {
qProfileStatusRepository.get(profile.getQpKey()).ifPresent(status -> {
if (status.equals(ADDED)) {
markAsAdded(profile);
} else if (status.equals(UPDATED)) {
markAsChanged(baseProfiles.get(profile.getQpKey()), profile);
markAsChanged(baseProfiles.get(profile.getQpKey()), profile, componentUuid);
}
});
}
}

private void markAsChanged(QualityProfile baseProfile, QualityProfile profile) {
Date from = baseProfile.getRulesUpdatedAt();
private void markAsChanged(QualityProfile baseProfile, QualityProfile profile, String componentUuid) {
try {
Map<ActiveRuleChange.Type, Long> changesMappedToNumberOfRules = qualityProfileRuleChangeTextResolver.mapChangeToNumberOfRules(baseProfile, componentUuid);

String data = KeyValueFormat.format(ImmutableSortedMap.of(
"key", profile.getQpKey(),
"from", UtcDateUtils.formatDateTime(fixDate(from)),
"to", UtcDateUtils.formatDateTime(fixDate(profile.getRulesUpdatedAt()))));
eventRepository.add(createQProfileEvent(profile, "Changes in %s", data));
if (changesMappedToNumberOfRules.isEmpty()) {
LOG.debug("No changes found for Quality Profile {}. Quality Profile event skipped.", profile.getQpKey());
return;
}

String data = KeyValueFormat.format(ImmutableSortedMap.of(
"key", profile.getQpKey(),
"from", UtcDateUtils.formatDateTime(fixDate(baseProfile.getRulesUpdatedAt())),
"to", UtcDateUtils.formatDateTime(fixDate(profile.getRulesUpdatedAt())),
"name", profile.getQpName(),
"languageKey", profile.getLanguageKey()));
String ruleChangeText = QualityProfileTextGenerator.generateRuleChangeText(changesMappedToNumberOfRules);

eventRepository.add(createQProfileEvent(profile, "%s updated with " + ruleChangeText, data, ruleChangeText));
} catch (Exception e) {
LOG.error("Failed to generate 'change' event for Quality Profile " + profile.getQpKey(), e);
}
}

private void markAsRemoved(QualityProfile profile) {
@@ -149,10 +170,14 @@ public class QualityProfileEventsStep implements ComputationStep {
return Event.createProfile(String.format(namePattern, profileLabel(profile)), data, null);
}

private Event createQProfileEvent(QualityProfile profile, String namePattern, @Nullable String data, @Nullable String description) {
return Event.createProfile(String.format(namePattern, profileLabel(profile)), data, description);
}

private String profileLabel(QualityProfile profile) {
Optional<Language> language = languageRepository.find(profile.getLanguageKey());
String languageName = language.isPresent() ? language.get().getName() : profile.getLanguageKey();
return String.format("'%s' (%s)", profile.getQpName(), languageName);
return String.format("\"%s\" (%s)", profile.getQpName(), languageName);
}

/**

+ 296
- 0
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileRuleChangeResolverTest.java Voir le fichier

@@ -0,0 +1,296 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.ce.task.projectanalysis.qualityprofile;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Suite;
import org.sonar.api.rules.CleanCodeAttribute;
import org.sonar.api.utils.KeyValueFormat;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.SnapshotDao;
import org.sonar.db.component.SnapshotDto;
import org.sonar.db.qualityprofile.QProfileChangeDao;
import org.sonar.db.qualityprofile.QProfileChangeDto;
import org.sonar.db.qualityprofile.QProfileChangeQuery;
import org.sonar.db.rule.RuleChangeDto;
import org.sonar.server.qualityprofile.ActiveRuleChange;
import org.sonar.server.qualityprofile.QualityProfile;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED;
import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.DEACTIVATED;
import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.UPDATED;

@RunWith(Suite.class)
@Suite.SuiteClasses({
QualityProfileRuleChangeResolverTest.TextResolutionTest.class,
QualityProfileRuleChangeResolverTest.ExceptionTest.class
})
public class QualityProfileRuleChangeResolverTest {
private final static String COMPONENT_UUID = "123";

@RunWith(Parameterized.class)
public static class TextResolutionTest {
private final DbClient dbClient = mock(DbClient.class);
private final DbSession dbSession = mock(DbSession.class);
private final QProfileChangeDao qProfileChangeDao = mock(QProfileChangeDao.class);
private final SnapshotDao snapshotDao = mock(SnapshotDao.class);
private final QualityProfile qualityProfile = mock(QualityProfile.class);

private final QualityProfileRuleChangeResolver underTest = new QualityProfileRuleChangeResolver(dbClient);

private final List<QProfileChangeDto> changes;
private final Map<ActiveRuleChange.Type, Long> expectedMap;


public TextResolutionTest(List<QProfileChangeDto> changes, Map<ActiveRuleChange.Type, Long> expectedMap) {
this.changes = changes;
this.expectedMap = expectedMap;
}

@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{
List.of(
createChange(ACTIVATED, "ruleUuid1", 123L),
createChange(DEACTIVATED, "ruleUuid2", 124L),
createChange(UPDATED, "ruleUuid3", 125L)
),
Map.ofEntries(
Map.entry(ACTIVATED, 1L),
Map.entry(DEACTIVATED, 1L),
Map.entry(UPDATED, 1L)
)
},
{
List.of(
createChange(ACTIVATED, "ruleUuid1", 123L),
createChange(DEACTIVATED, "ruleUuid1", 124L), // should cancel previous change
createChange(UPDATED, "ruleUuid2", 125L)
),
Map.ofEntries(
Map.entry(UPDATED, 1L)
)
},
{
List.of(
createChange(ACTIVATED, "ruleUuid1", 123L),
createChange(DEACTIVATED, "ruleUuid1", 124L), // should cancel previous change
createChange(ACTIVATED, "ruleUuid1", 125L),
createChange(UPDATED, "ruleUuid2", 126L)
),
Map.ofEntries(
Map.entry(ACTIVATED, 1L),
Map.entry(UPDATED, 1L)
)
},
{
List.of(
createChange(ACTIVATED, "ruleUuid1", 123L),
createCCTUpdate("ruleUuid1", 130L), // should overwrite previous change
createChange(DEACTIVATED, "ruleUuid2", 124L),
createChange(DEACTIVATED, "ruleUuid3", 125L),
createChange(UPDATED, "ruleUuid4", 126L)
),
Map.ofEntries(
Map.entry(DEACTIVATED, 2L),
Map.entry(UPDATED, 2L)
)
},
{
List.of(
createChange(ACTIVATED, "ruleUuid1", 123L),
createChange(UPDATED, "ruleUuid1", 130L),
createChange(DEACTIVATED, "ruleUuid1", 131L), // should overwrite update and cancel out the activation resulting to no change
createCCTUpdate("ruleUuid2", 126L)
),
Map.ofEntries(
Map.entry(UPDATED, 1L)
)
},
{
List.of(
createChange(DEACTIVATED, "ruleUuid1", 123L),
createChange(UPDATED, "ruleUuid1", 130L),
createChange(UPDATED, "ruleUuid2", 126L)
),
Map.ofEntries(
Map.entry(UPDATED, 2L)
)
},
{
// single CCT change
List.of(
createCCTUpdate("ruleUuid1", 123L)
),
Map.ofEntries(
Map.entry(UPDATED, 1L)
)
},
{
// multiple CCT changes
List.of(
createCCTUpdate("ruleUuid1", 123L),
createCCTUpdate("ruleUuid2", 124L)
),
Map.ofEntries(
Map.entry(UPDATED, 2L)
)
},
{
// mixed CCT and old taxonomy changes
List.of(
createCCTUpdate("ruleUuid1", 123L),
createChange(ACTIVATED, "ruleUuid2", 124L),
createCCTUpdate("ruleUuid3", 125L),
createChange(DEACTIVATED, "ruleUuid4", 126L),
createChange(UPDATED, "ruleUuid3", 127L)
),
Map.ofEntries(
Map.entry(ACTIVATED, 1L),
Map.entry(DEACTIVATED, 1L),
Map.entry(UPDATED, 2L)
)
},
{
List.of(
createChange(ACTIVATED, "ruleUuid1", 123L),
createChange(DEACTIVATED, "ruleUuid1", 124L) // should cancel previous change
),
Map.ofEntries()
}
});
}

@Before
public void setUp() {
when(dbClient.openSession(false)).thenReturn(dbSession);
doReturn(qProfileChangeDao).when(dbClient).qProfileChangeDao();

SnapshotDto snapshotDto = new SnapshotDto()
.setAnalysisDate(123L);
doReturn(Optional.of(snapshotDto)).when(snapshotDao).selectLastAnalysisByComponentUuid(dbSession, COMPONENT_UUID);
doReturn(snapshotDao).when(dbClient).snapshotDao();

doReturn("profileUuid").when(qualityProfile).getQpKey();
doReturn("profileName").when(qualityProfile).getQpName();
}

@Test
public void givenQPChanges_whenResolveText_thenResolvedTextContainsAll() {
// given
doReturn(changes).when(qProfileChangeDao).selectByQuery(eq(dbSession), any(QProfileChangeQuery.class));

// when
Map<ActiveRuleChange.Type, Long> changeToNumberOfRules = underTest.mapChangeToNumberOfRules(qualityProfile, COMPONENT_UUID);

// then
assertThat(changeToNumberOfRules).isEqualTo(expectedMap);
}
}

public static class ExceptionTest {

private final DbClient dbClient = mock(DbClient.class);
private final DbSession dbSession = mock(DbSession.class);
private final QProfileChangeDao qProfileChangeDao = mock(QProfileChangeDao.class);
private final SnapshotDao snapshotDao = mock(SnapshotDao.class);

private final QualityProfile qualityProfile = mock(QualityProfile.class);

private final QualityProfileRuleChangeResolver underTest = new QualityProfileRuleChangeResolver(dbClient);


@Before
public void setUp() {
when(dbClient.openSession(false)).thenReturn(dbSession);
doReturn(qProfileChangeDao).when(dbClient).qProfileChangeDao();

SnapshotDto snapshotDto = new SnapshotDto()
.setAnalysisDate(123L);
doReturn(Optional.of(snapshotDto)).when(snapshotDao).selectLastAnalysisByComponentUuid(dbSession, COMPONENT_UUID);
doReturn(snapshotDao).when(dbClient).snapshotDao();

doReturn("profileUuid").when(qualityProfile).getQpKey();
doReturn("profileName").when(qualityProfile).getQpName();
}

@Test
public void givenNoQPChanges_whenResolveText_thenThrows() {
// given
doReturn(List.of()).when(qProfileChangeDao).selectByQuery(eq(dbSession), any(QProfileChangeQuery.class));

// when then
assertThatThrownBy(() -> underTest.mapChangeToNumberOfRules(qualityProfile, COMPONENT_UUID))
.isInstanceOf(IllegalStateException.class)
.hasMessage("No profile changes found for profileName");
}

@Test
public void givenNoSnapshotFound_whenResolveText_thenThrows() {
// given
doReturn(Optional.empty()).when(snapshotDao).selectLastAnalysisByComponentUuid(dbSession, COMPONENT_UUID);

// when then
assertThatThrownBy(() -> underTest.mapChangeToNumberOfRules(qualityProfile, COMPONENT_UUID))
.isInstanceOf(IllegalStateException.class)
.hasMessage("No snapshot found for 123");
}
}

private static QProfileChangeDto createChange(ActiveRuleChange.Type type, String ruleUuid, Long createdAt) {
return new QProfileChangeDto()
.setUuid("uuid")
.setCreatedAt(createdAt)
.setRulesProfileUuid("ruleProfileUuid")
.setChangeType(type.name())
.setData(KeyValueFormat.parse("ruleUuid=" + ruleUuid));
}

private static QProfileChangeDto createCCTUpdate(String ruleUuid, Long createdAt) {
RuleChangeDto ruleChangeDto = new RuleChangeDto();
ruleChangeDto.setOldCleanCodeAttribute(CleanCodeAttribute.CONVENTIONAL);
ruleChangeDto.setNewCleanCodeAttribute(CleanCodeAttribute.CLEAR);
ruleChangeDto.setRuleUuid(ruleUuid);
return new QProfileChangeDto()
.setUuid("uuid")
.setCreatedAt(createdAt)
.setRulesProfileUuid("ruleProfileUuid")
.setChangeType(UPDATED.name())
.setRuleChange(ruleChangeDto);
}

}

+ 101
- 0
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualityprofile/QualityProfileTextGeneratorTest.java Voir le fichier

@@ -0,0 +1,101 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.ce.task.projectanalysis.qualityprofile;

import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.sonar.server.qualityprofile.ActiveRuleChange;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(Parameterized.class)
public class QualityProfileTextGeneratorTest {

private final Map<ActiveRuleChange.Type, Long> changeToNumberOfRules;
private final String expectedText;

public QualityProfileTextGeneratorTest(Map<ActiveRuleChange.Type, Long> changeToNumberOfRules, String expectedText) {
this.changeToNumberOfRules = changeToNumberOfRules;
this.expectedText = expectedText;
}

@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{
Map.ofEntries(
Map.entry(ActiveRuleChange.Type.ACTIVATED, 12L),
Map.entry(ActiveRuleChange.Type.DEACTIVATED, 8L),
Map.entry(ActiveRuleChange.Type.UPDATED, 5L)
),
"12 new rules, 8 deactivated rules and 5 modified rules"
},
{
Map.ofEntries(
Map.entry(ActiveRuleChange.Type.ACTIVATED, 1L),
Map.entry(ActiveRuleChange.Type.DEACTIVATED, 0L),
Map.entry(ActiveRuleChange.Type.UPDATED, 0L)
),
"1 new rule"
},
{
Map.ofEntries(
Map.entry(ActiveRuleChange.Type.DEACTIVATED, 5L)
),
"5 deactivated rules"
},
{
Map.ofEntries(
Map.entry(ActiveRuleChange.Type.UPDATED, 7L)
),
"7 modified rules"
},
{
Map.ofEntries(
Map.entry(ActiveRuleChange.Type.ACTIVATED, 1L),
Map.entry(ActiveRuleChange.Type.DEACTIVATED, 1L),
Map.entry(ActiveRuleChange.Type.UPDATED, 1L)
),
"1 new rule, 1 deactivated rule and 1 modified rule"
},
{
Map.ofEntries(
Map.entry(ActiveRuleChange.Type.ACTIVATED, 1L),
Map.entry(ActiveRuleChange.Type.UPDATED, 3L)
),
"1 new rule and 3 modified rules"
}
});
}

@Test
public void givenRulesChanges_whenBuild_thenTextContainsAll() {
// given when
String updateMessage = QualityProfileTextGenerator.generateRuleChangeText(changeToNumberOfRules);

// then
assertThat(updateMessage).isEqualTo(expectedText);
}

}

+ 130
- 34
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/QualityProfileEventsStepTest.java Voir le fichier

@@ -24,6 +24,7 @@ import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
@@ -31,9 +32,11 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.slf4j.event.Level;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.resources.AbstractLanguage;
import org.sonar.api.resources.Language;
import org.sonar.api.testfixtures.log.LogTester;
import org.sonar.ce.task.projectanalysis.component.Component;
import org.sonar.ce.task.projectanalysis.component.ReportComponent;
import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
@@ -46,8 +49,10 @@ import org.sonar.ce.task.projectanalysis.metric.Metric;
import org.sonar.ce.task.projectanalysis.metric.MetricRepository;
import org.sonar.ce.task.projectanalysis.qualityprofile.MutableQProfileStatusRepository;
import org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepositoryImpl;
import org.sonar.ce.task.projectanalysis.qualityprofile.QualityProfileRuleChangeResolver;
import org.sonar.ce.task.step.TestComputationStepContext;
import org.sonar.core.util.UtcDateUtils;
import org.sonar.server.qualityprofile.ActiveRuleChange;
import org.sonar.server.qualityprofile.QPMeasureData;
import org.sonar.server.qualityprofile.QualityProfile;

@@ -67,8 +72,20 @@ import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRep
import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository.Status.UPDATED;

public class QualityProfileEventsStepTest {
private static final Date BEFORE_DATE = parseDateTime("2011-04-25T01:05:13+0100");
private static final Date AFTER_DATE = parseDateTime("2011-04-25T01:05:17+0100");
private static final Date BEFORE_DATE_PLUS_1_SEC = parseDateTime("2011-04-25T01:05:14+0100");
private static final Date AFTER_DATE_PLUS_1_SEC = parseDateTime("2011-04-25T01:05:18+0100");
private static final String RULE_CHANGE_TEXT = "1 new rule, 2 deactivated rules and 3 modified rules";
private static final Map<ActiveRuleChange.Type, Long> CHANGE_TO_NUMBER_OF_RULES_MAP = Map.ofEntries(
Map.entry(ActiveRuleChange.Type.ACTIVATED, 1L),
Map.entry(ActiveRuleChange.Type.DEACTIVATED, 2L),
Map.entry(ActiveRuleChange.Type.UPDATED, 3L)
);
@Rule
public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
@Rule
public LogTester logTester = new LogTester();

private static final String QP_NAME_1 = "qp_1";
private static final String QP_NAME_2 = "qp_2";
@@ -76,16 +93,16 @@ public class QualityProfileEventsStepTest {
private static final String LANGUAGE_KEY_2 = "language_key_2";
private static final String LANGUAGE_KEY_3 = "languageKey3";

private MetricRepository metricRepository = mock(MetricRepository.class);
private MeasureRepository measureRepository = mock(MeasureRepository.class);
private LanguageRepository languageRepository = mock(LanguageRepository.class);
private EventRepository eventRepository = mock(EventRepository.class);
private ArgumentCaptor<Event> eventArgumentCaptor = ArgumentCaptor.forClass(Event.class);
private MutableQProfileStatusRepository qProfileStatusRepository = new QProfileStatusRepositoryImpl();

private Metric qualityProfileMetric = mock(Metric.class);
private final MetricRepository metricRepository = mock(MetricRepository.class);
private final MeasureRepository measureRepository = mock(MeasureRepository.class);
private final LanguageRepository languageRepository = mock(LanguageRepository.class);
private final EventRepository eventRepository = mock(EventRepository.class);
private final ArgumentCaptor<Event> eventArgumentCaptor = ArgumentCaptor.forClass(Event.class);
private final MutableQProfileStatusRepository qProfileStatusRepository = new QProfileStatusRepositoryImpl();

private QualityProfileEventsStep underTest = new QualityProfileEventsStep(treeRootHolder, metricRepository, measureRepository, languageRepository, eventRepository, qProfileStatusRepository);
private final Metric qualityProfileMetric = mock(Metric.class);
private final QualityProfileRuleChangeResolver qualityProfileRuleChangeTextResolver = mock(QualityProfileRuleChangeResolver.class);
private final QualityProfileEventsStep underTest = new QualityProfileEventsStep(treeRootHolder, metricRepository, measureRepository, languageRepository, eventRepository, qProfileStatusRepository, qualityProfileRuleChangeTextResolver);

@Before
public void setUp() {
@@ -114,7 +131,7 @@ public class QualityProfileEventsStepTest {

@Test
public void no_event_if_no_base_and_quality_profile_measure_is_empty() {
mockMeasures(treeRootHolder.getRoot(), null, null);
mockQualityProfileMeasures(treeRootHolder.getRoot(), null, null);

underTest.execute(new TestComputationStepContext());

@@ -127,13 +144,13 @@ public class QualityProfileEventsStepTest {
qProfileStatusRepository.register(qp.getQpKey(), ADDED);

Language language = mockLanguageInRepository(LANGUAGE_KEY_1);
mockMeasures(treeRootHolder.getRoot(), null, arrayOf(qp));
mockQualityProfileMeasures(treeRootHolder.getRoot(), null, arrayOf(qp));

underTest.execute(new TestComputationStepContext());

verify(eventRepository).add(eventArgumentCaptor.capture());
verifyNoMoreInteractions(eventRepository);
verifyEvent(eventArgumentCaptor.getValue(), "Use '" + qp.getQpName() + "' (" + language.getName() + ")", null);
verifyEvent(eventArgumentCaptor.getValue(), "Use \"" + qp.getQpName() + "\" (" + language.getName() + ")", null, null);
}

@Test
@@ -142,13 +159,13 @@ public class QualityProfileEventsStepTest {
qProfileStatusRepository.register(qp.getQpKey(), ADDED);

mockLanguageNotInRepository(LANGUAGE_KEY_1);
mockMeasures(treeRootHolder.getRoot(), null, arrayOf(qp));
mockQualityProfileMeasures(treeRootHolder.getRoot(), null, arrayOf(qp));

underTest.execute(new TestComputationStepContext());

verify(eventRepository).add(eventArgumentCaptor.capture());
verifyNoMoreInteractions(eventRepository);
verifyEvent(eventArgumentCaptor.getValue(), "Use '" + qp.getQpName() + "' (" + qp.getLanguageKey() + ")", null);
verifyEvent(eventArgumentCaptor.getValue(), "Use \"" + qp.getQpName() + "\" (" + qp.getLanguageKey() + ")", null, null);
}

@Test
@@ -156,35 +173,35 @@ public class QualityProfileEventsStepTest {
QualityProfile qp = qp(QP_NAME_1, LANGUAGE_KEY_1, new Date());
qProfileStatusRepository.register(qp.getQpKey(), REMOVED);

mockMeasures(treeRootHolder.getRoot(), arrayOf(qp), null);
mockQualityProfileMeasures(treeRootHolder.getRoot(), arrayOf(qp), null);
Language language = mockLanguageInRepository(LANGUAGE_KEY_1);

underTest.execute(new TestComputationStepContext());

verify(eventRepository).add(eventArgumentCaptor.capture());
verifyNoMoreInteractions(eventRepository);
verifyEvent(eventArgumentCaptor.getValue(), "Stop using '" + qp.getQpName() + "' (" + language.getName() + ")", null);
verifyEvent(eventArgumentCaptor.getValue(), "Stop using \"" + qp.getQpName() + "\" (" + language.getName() + ")", null, null);
}

@Test
public void no_more_used_event_uses_language_key_in_message_if_language_not_found() {
QualityProfile qp = qp(QP_NAME_1, LANGUAGE_KEY_1, new Date());
qProfileStatusRepository.register(qp.getQpKey(), REMOVED);
mockMeasures(treeRootHolder.getRoot(), arrayOf(qp), null);
mockQualityProfileMeasures(treeRootHolder.getRoot(), arrayOf(qp), null);
mockLanguageNotInRepository(LANGUAGE_KEY_1);

underTest.execute(new TestComputationStepContext());

verify(eventRepository).add(eventArgumentCaptor.capture());
verifyNoMoreInteractions(eventRepository);
verifyEvent(eventArgumentCaptor.getValue(), "Stop using '" + qp.getQpName() + "' (" + qp.getLanguageKey() + ")", null);
verifyEvent(eventArgumentCaptor.getValue(), "Stop using \"" + qp.getQpName() + "\" (" + qp.getLanguageKey() + ")", null, null);
}

@Test
public void no_event_if_qp_is_unchanged() {
QualityProfile qp = qp(QP_NAME_1, LANGUAGE_KEY_1, new Date());
qProfileStatusRepository.register(qp.getQpKey(), UNCHANGED);
mockMeasures(treeRootHolder.getRoot(), arrayOf(qp), arrayOf(qp));
mockQualityProfileMeasures(treeRootHolder.getRoot(), arrayOf(qp), arrayOf(qp));

underTest.execute(new TestComputationStepContext());

@@ -193,20 +210,97 @@ public class QualityProfileEventsStepTest {

@Test
public void changed_event_if_qp_has_been_updated() {
QualityProfile qp1 = qp(QP_NAME_1, LANGUAGE_KEY_1, parseDateTime("2011-04-25T01:05:13+0100"));
QualityProfile qp2 = qp(QP_NAME_1, LANGUAGE_KEY_1, parseDateTime("2011-04-25T01:05:17+0100"));
QualityProfile qp1 = qp(QP_NAME_1, LANGUAGE_KEY_1, BEFORE_DATE);
QualityProfile qp2 = qp(QP_NAME_1, LANGUAGE_KEY_1, AFTER_DATE);
qProfileStatusRepository.register(qp2.getQpKey(), UPDATED);
mockMeasures(treeRootHolder.getRoot(), arrayOf(qp1), arrayOf(qp2));
mockQualityProfileMeasures(treeRootHolder.getRoot(), arrayOf(qp1), arrayOf(qp2));
Language language = mockLanguageInRepository(LANGUAGE_KEY_1);

when(qualityProfileRuleChangeTextResolver.mapChangeToNumberOfRules(qp2, treeRootHolder.getRoot().getUuid())).thenReturn(CHANGE_TO_NUMBER_OF_RULES_MAP);

underTest.execute(new TestComputationStepContext());

verify(eventRepository).add(eventArgumentCaptor.capture());
verifyNoMoreInteractions(eventRepository);
verifyEvent(eventArgumentCaptor.getValue(),
"Changes in '" + qp2.getQpName() + "' (" + language.getName() + ")",
"from=" + UtcDateUtils.formatDateTime(parseDateTime("2011-04-25T01:05:14+0100")) + ";key=" + qp1.getQpKey() + ";to="
+ UtcDateUtils.formatDateTime(parseDateTime("2011-04-25T01:05:18+0100")));
"\"" + qp2.getQpName() + "\" (" + language.getName() + ") updated with " + RULE_CHANGE_TEXT,
"from=" + UtcDateUtils.formatDateTime(BEFORE_DATE_PLUS_1_SEC) +
";key=" + qp1.getQpKey() +
";languageKey=" + qp2.getLanguageKey()+
";name=" + qp2.getQpName() +
";to=" + UtcDateUtils.formatDateTime(AFTER_DATE_PLUS_1_SEC),
RULE_CHANGE_TEXT);
}

@Test
public void givenRulesWhereAddedModifiedOrRemoved_whenEventStep_thenQPChangeEventIsAddedWithDetails() {
QualityProfile qp1 = qp(QP_NAME_1, LANGUAGE_KEY_1, BEFORE_DATE);
QualityProfile qp2 = qp(QP_NAME_1, LANGUAGE_KEY_1, AFTER_DATE);

// mock updated profile
qProfileStatusRepository.register(qp2.getQpKey(), UPDATED);
mockQualityProfileMeasures(treeRootHolder.getRoot(), arrayOf(qp1), arrayOf(qp2));
Language language = mockLanguageInRepository(LANGUAGE_KEY_1);

// mock rule changes
when(qualityProfileRuleChangeTextResolver.mapChangeToNumberOfRules(qp2, treeRootHolder.getRoot().getUuid())).thenReturn(CHANGE_TO_NUMBER_OF_RULES_MAP);

underTest.execute(new TestComputationStepContext());

verify(eventRepository).add(eventArgumentCaptor.capture());
verifyNoMoreInteractions(eventRepository);
verifyEvent(eventArgumentCaptor.getValue(),
"\"" + qp2.getQpName() + "\" (" + language.getName() + ") updated with " + RULE_CHANGE_TEXT,
"from=" + UtcDateUtils.formatDateTime(BEFORE_DATE_PLUS_1_SEC) +
";key=" + qp1.getQpKey() +
";languageKey=" + qp2.getLanguageKey()+
";name=" + qp2.getQpName() +
";to=" + UtcDateUtils.formatDateTime(AFTER_DATE_PLUS_1_SEC),
RULE_CHANGE_TEXT);
}

@Test
public void givenRuleTextResolverException_whenEventStep_thenLogAndContinue() {
// given
logTester.setLevel(Level.ERROR);
QualityProfile existingQP = qp(QP_NAME_1, LANGUAGE_KEY_1, BEFORE_DATE);
QualityProfile newQP = qp(QP_NAME_1, LANGUAGE_KEY_1, AFTER_DATE);

// mock updated profile
qProfileStatusRepository.register(newQP.getQpKey(), UPDATED);
mockQualityProfileMeasures(treeRootHolder.getRoot(), arrayOf(existingQP), arrayOf(newQP));

when(qualityProfileRuleChangeTextResolver.mapChangeToNumberOfRules(newQP, treeRootHolder.getRoot().getUuid())).thenThrow(new RuntimeException("error"));
var context = new TestComputationStepContext();

// when
underTest.execute(context);

// then
assertThat(logTester.logs(Level.ERROR)).containsExactly("Failed to generate 'change' event for Quality Profile " + newQP.getQpKey());
verify(eventRepository, never()).add(any(Event.class));
}

@Test
public void givenNoChangesFound_whenEventStep_thenDebugLogAndSkipEvent() {
// given
logTester.setLevel(Level.DEBUG);
QualityProfile existingQP = qp(QP_NAME_1, LANGUAGE_KEY_1, BEFORE_DATE);
QualityProfile newQP = qp(QP_NAME_1, LANGUAGE_KEY_1, AFTER_DATE);

// mock updated profile
qProfileStatusRepository.register(newQP.getQpKey(), UPDATED);
mockQualityProfileMeasures(treeRootHolder.getRoot(), arrayOf(existingQP), arrayOf(newQP));

when(qualityProfileRuleChangeTextResolver.mapChangeToNumberOfRules(newQP, treeRootHolder.getRoot().getUuid())).thenReturn(Collections.emptyMap());
var context = new TestComputationStepContext();

// when
underTest.execute(context);

// then
assertThat(logTester.logs(Level.DEBUG)).containsExactly("No changes found for Quality Profile " + newQP.getQpKey() + ". Quality Profile event skipped.");
verify(eventRepository, never()).add(any(Event.class));
}

@Test
@@ -220,11 +314,11 @@ public class QualityProfileEventsStepTest {
Date date = new Date();
QualityProfile qp1 = qp(QP_NAME_2, LANGUAGE_KEY_1, date);
QualityProfile qp2 = qp(QP_NAME_2, LANGUAGE_KEY_2, date);
QualityProfile qp3 = qp(QP_NAME_1, LANGUAGE_KEY_1, parseDateTime("2011-04-25T01:05:13+0100"));
QualityProfile qp3_updated = qp(QP_NAME_1, LANGUAGE_KEY_1, parseDateTime("2011-04-25T01:05:17+0100"));
QualityProfile qp3 = qp(QP_NAME_1, LANGUAGE_KEY_1, BEFORE_DATE);
QualityProfile qp3_updated = qp(QP_NAME_1, LANGUAGE_KEY_1, AFTER_DATE);
QualityProfile qp4 = qp(QP_NAME_2, LANGUAGE_KEY_3, date);

mockMeasures(
mockQualityProfileMeasures(
treeRootHolder.getRoot(),
arrayOf(qp1, qp2, qp3),
arrayOf(qp3_updated, qp2, qp4));
@@ -234,12 +328,14 @@ public class QualityProfileEventsStepTest {
qProfileStatusRepository.register(qp3.getQpKey(), UPDATED);
qProfileStatusRepository.register(qp4.getQpKey(), ADDED);

when(qualityProfileRuleChangeTextResolver.mapChangeToNumberOfRules(qp3_updated, treeRootHolder.getRoot().getUuid())).thenReturn(CHANGE_TO_NUMBER_OF_RULES_MAP);

underTest.execute(new TestComputationStepContext());

assertThat(events).extracting("name").containsOnly(
"Stop using '" + QP_NAME_2 + "' (" + LANGUAGE_KEY_1 + ")",
"Use '" + QP_NAME_2 + "' (" + LANGUAGE_KEY_3 + ")",
"Changes in '" + QP_NAME_1 + "' (" + LANGUAGE_KEY_1 + ")");
"Stop using \"" + QP_NAME_2 + "\" (" + LANGUAGE_KEY_1 + ")",
"Use \"" + QP_NAME_2 + "\" (" + LANGUAGE_KEY_3 + ")",
"\"" + QP_NAME_1 + "\" (" + LANGUAGE_KEY_1 + ") updated with " + RULE_CHANGE_TEXT);
}

private Language mockLanguageInRepository(String languageKey) {
@@ -261,16 +357,16 @@ public class QualityProfileEventsStepTest {
when(languageRepository.find(anyString())).thenReturn(Optional.empty());
}

private void mockMeasures(Component component, @Nullable QualityProfile[] previous, @Nullable QualityProfile[] current) {
private void mockQualityProfileMeasures(Component component, @Nullable QualityProfile[] previous, @Nullable QualityProfile[] current) {
when(measureRepository.getBaseMeasure(component, qualityProfileMetric)).thenReturn(Optional.of(newMeasure(previous)));
when(measureRepository.getRawMeasure(component, qualityProfileMetric)).thenReturn(Optional.of(newMeasure(current)));
}

private static void verifyEvent(Event event, String expectedName, @Nullable String expectedData) {
private static void verifyEvent(Event event, String expectedName, @Nullable String expectedData, @Nullable String expectedDescription) {
assertThat(event.getName()).isEqualTo(expectedName);
assertThat(event.getData()).isEqualTo(expectedData);
assertThat(event.getCategory()).isEqualTo(Event.Category.PROFILE);
assertThat(event.getDescription()).isNull();
assertThat(event.getDescription()).isEqualTo(expectedDescription);
}

private static QualityProfile qp(String qpName, String languageKey, Date date) {

Chargement…
Annuler
Enregistrer