aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-ce-task-projectanalysis/src
diff options
context:
space:
mode:
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>2019-04-16 15:19:27 +0200
committersonartech <sonartech@sonarsource.com>2019-04-23 10:37:57 +0200
commit58bb4b37da6e32a113870b0fc98d5494379641b6 (patch)
tree027172a0a7945ea6b2f3dae5d917e1fce1a66c81 /server/sonar-ce-task-projectanalysis/src
parentd9e7cb020409491b45199ab8762eb22746e3543d (diff)
downloadsonarqube-58bb4b37da6e32a113870b0fc98d5494379641b6.tar.gz
sonarqube-58bb4b37da6e32a113870b0fc98d5494379641b6.zip
SONAR-11757 single notification for FPs and changes on my issues
Diffstat (limited to 'server/sonar-ce-task-projectanalysis/src')
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java4
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/notification/NotificationFactory.java (renamed from server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/notification/NewIssuesNotificationFactory.java)68
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/notification/ReportAnalysisFailureNotificationEmailTemplate.java4
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStep.java92
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/DumbRule.java1
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/notification/NotificationFactoryTest.java (renamed from server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/notification/NewIssuesNotificationFactoryTest.java)401
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStepTest.java154
7 files changed, 602 insertions, 122 deletions
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 7afdefb8eeb..3298ce5abc0 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
@@ -99,7 +99,7 @@ import org.sonar.ce.task.projectanalysis.measure.MeasureComputersVisitor;
import org.sonar.ce.task.projectanalysis.measure.MeasureRepositoryImpl;
import org.sonar.ce.task.projectanalysis.measure.MeasureToMeasureDto;
import org.sonar.ce.task.projectanalysis.metric.MetricModule;
-import org.sonar.ce.task.projectanalysis.notification.NewIssuesNotificationFactory;
+import org.sonar.ce.task.projectanalysis.notification.NotificationFactory;
import org.sonar.ce.task.projectanalysis.organization.DefaultOrganizationLoader;
import org.sonar.ce.task.projectanalysis.period.PeriodHolderImpl;
import org.sonar.ce.task.projectanalysis.qualitygate.EvaluationResultTextConverterImpl;
@@ -306,7 +306,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop
WebhookPostTask.class,
// notifications
- NewIssuesNotificationFactory.class);
+ NotificationFactory.class);
}
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/notification/NewIssuesNotificationFactory.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/notification/NotificationFactory.java
index 60b2629c612..1bf255cd626 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/notification/NewIssuesNotificationFactory.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/notification/NotificationFactory.java
@@ -22,35 +22,56 @@ package org.sonar.ce.task.projectanalysis.notification;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.utils.Durations;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
+import org.sonar.ce.task.projectanalysis.analysis.Branch;
import org.sonar.ce.task.projectanalysis.component.Component;
import org.sonar.ce.task.projectanalysis.component.DepthTraversalTypeAwareCrawler;
import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
import org.sonar.ce.task.projectanalysis.component.TypeAwareVisitorAdapter;
import org.sonar.ce.task.projectanalysis.issue.RuleRepository;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.db.user.UserDto;
+import org.sonar.server.issue.notification.IssuesChangesNotification;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
+import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer;
import org.sonar.server.issue.notification.MyNewIssuesNotification;
import org.sonar.server.issue.notification.NewIssuesNotification;
import org.sonar.server.issue.notification.NewIssuesNotification.DetailsSupplier;
import org.sonar.server.issue.notification.NewIssuesNotification.RuleDefinition;
+import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.PRE_ORDER;
import static org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit.FILE;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
@ComputeEngineSide
-public class NewIssuesNotificationFactory {
+public class NotificationFactory {
private final TreeRootHolder treeRootHolder;
+ private final AnalysisMetadataHolder analysisMetadataHolder;
private final RuleRepository ruleRepository;
private final Durations durations;
+ private final IssuesChangesNotificationSerializer issuesChangesSerializer;
private Map<String, Component> componentsByUuid;
- public NewIssuesNotificationFactory(TreeRootHolder treeRootHolder, RuleRepository ruleRepository, Durations durations) {
+ public NotificationFactory(TreeRootHolder treeRootHolder, AnalysisMetadataHolder analysisMetadataHolder,
+ RuleRepository ruleRepository, Durations durations, IssuesChangesNotificationSerializer issuesChangesSerializer) {
this.treeRootHolder = treeRootHolder;
+ this.analysisMetadataHolder = analysisMetadataHolder;
this.ruleRepository = ruleRepository;
this.durations = durations;
+ this.issuesChangesSerializer = issuesChangesSerializer;
}
public MyNewIssuesNotification newMyNewIssuesNotification(Map<String, UserDto> assigneesByUuid) {
@@ -63,6 +84,49 @@ public class NewIssuesNotificationFactory {
return new NewIssuesNotification(durations, new DetailsSupplierImpl(assigneesByUuid));
}
+ public IssuesChangesNotification newIssuesChangesNotification(Set<DefaultIssue> issues, Map<String, UserDto> assigneesByUuid) {
+ AnalysisChange change = new AnalysisChange(analysisMetadataHolder.getAnalysisDate());
+ Set<ChangedIssue> changedIssues = issues.stream()
+ .map(issue -> new ChangedIssue.Builder(issue.key())
+ .setAssignee(getAssignee(issue.assignee(), assigneesByUuid))
+ .setNewResolution(issue.resolution())
+ .setNewStatus(issue.status())
+ .setRule(getRuleByRuleKey(issue.ruleKey()))
+ .setProject(getProject())
+ .build())
+ .collect(MoreCollectors.toSet(issues.size()));
+
+ return issuesChangesSerializer.serialize(new IssuesChangesNotificationBuilder(changedIssues, change));
+ }
+
+ @CheckForNull
+ public User getAssignee(@Nullable String assigneeUuid, Map<String, UserDto> assigneesByUuid) {
+ if (assigneeUuid == null) {
+ return null;
+ }
+ UserDto dto = assigneesByUuid.get(assigneeUuid);
+ checkState(dto != null, "Can not find DTO for assignee uuid %s", assigneeUuid);
+ return new User(dto.getUuid(), dto.getLogin(), dto.getName());
+ }
+
+ private IssuesChangesNotificationBuilder.Rule getRuleByRuleKey(RuleKey ruleKey) {
+ return ruleRepository.findByKey(ruleKey)
+ .map(t -> new IssuesChangesNotificationBuilder.Rule(ruleKey, t.getName()))
+ .orElseThrow(() -> new IllegalStateException("Can not find rule " + ruleKey + " in RuleRepository"));
+ }
+
+ private Project getProject() {
+ Component project = treeRootHolder.getRoot();
+ Branch branch = analysisMetadataHolder.getBranch();
+ Project.Builder builder = new Project.Builder(project.getUuid())
+ .setKey(project.getKey())
+ .setProjectName(project.getName());
+ if (!branch.isLegacyFeature() && branch.getType() != PULL_REQUEST && !branch.isMain()) {
+ builder.setBranchName(branch.getName());
+ }
+ return builder.build();
+ }
+
private static void verifyAssigneesByUuid(Map<String, UserDto> assigneesByUuid) {
requireNonNull(assigneesByUuid, "assigneesByUuid can't be null");
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/notification/ReportAnalysisFailureNotificationEmailTemplate.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/notification/ReportAnalysisFailureNotificationEmailTemplate.java
index ef0ff26bd4b..280fdd3fd11 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/notification/ReportAnalysisFailureNotificationEmailTemplate.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/notification/ReportAnalysisFailureNotificationEmailTemplate.java
@@ -21,6 +21,7 @@ package org.sonar.ce.task.projectanalysis.notification;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
+import javax.annotation.CheckForNull;
import org.sonar.api.config.EmailSettings;
import org.sonar.api.notifications.Notification;
import org.sonar.server.issue.notification.EmailMessage;
@@ -41,6 +42,7 @@ public class ReportAnalysisFailureNotificationEmailTemplate implements EmailTemp
}
@Override
+ @CheckForNull
public EmailMessage format(Notification notification) {
if (!(notification instanceof ReportAnalysisFailureNotification)) {
return null;
@@ -53,7 +55,7 @@ public class ReportAnalysisFailureNotificationEmailTemplate implements EmailTemp
return new EmailMessage()
.setMessageId(notification.getType() + "/" + projectUuid)
.setSubject(subject(projectFullName))
- .setMessage(message(projectFullName, taskFailureNotification));
+ .setPlainTextMessage(message(projectFullName, taskFailureNotification));
}
private static String computeProjectFullName(ReportAnalysisFailureNotificationBuilder.Project project) {
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStep.java
index 0e98642046e..8393918369b 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStep.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStep.java
@@ -19,19 +19,17 @@
*/
package org.sonar.ce.task.projectanalysis.step;
-import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.Collection;
import java.util.Date;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
+import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import org.sonar.api.issue.Issue;
import org.sonar.api.notifications.Notification;
@@ -40,22 +38,17 @@ import org.sonar.api.utils.Duration;
import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
import org.sonar.ce.task.projectanalysis.analysis.Branch;
import org.sonar.ce.task.projectanalysis.component.Component;
-import org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit;
-import org.sonar.ce.task.projectanalysis.component.DepthTraversalTypeAwareCrawler;
import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
-import org.sonar.ce.task.projectanalysis.component.TypeAwareVisitorAdapter;
import org.sonar.ce.task.projectanalysis.issue.IssueCache;
-import org.sonar.ce.task.projectanalysis.issue.RuleRepository;
-import org.sonar.ce.task.projectanalysis.notification.NewIssuesNotificationFactory;
+import org.sonar.ce.task.projectanalysis.notification.NotificationFactory;
import org.sonar.ce.task.step.ComputationStep;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.util.CloseableIterator;
-import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.BranchType;
import org.sonar.db.user.UserDto;
-import org.sonar.server.issue.notification.IssueChangeNotification;
+import org.sonar.server.issue.notification.IssuesChangesNotification;
import org.sonar.server.issue.notification.MyNewIssuesNotification;
import org.sonar.server.issue.notification.NewIssuesNotification;
import org.sonar.server.issue.notification.NewIssuesStatistics;
@@ -64,9 +57,8 @@ import org.sonar.server.notification.NotificationService;
import static java.util.Collections.singleton;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
-import static java.util.stream.Collectors.toSet;
import static java.util.stream.StreamSupport.stream;
-import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER;
+import static org.sonar.core.util.stream.MoreCollectors.toSet;
import static org.sonar.db.component.BranchType.PULL_REQUEST;
import static org.sonar.db.component.BranchType.SHORT;
@@ -79,27 +71,25 @@ public class SendIssueNotificationsStep implements ComputationStep {
/**
* Types of the notifications sent by this step
*/
- static final Set<Class<? extends Notification>> NOTIF_TYPES = ImmutableSet.of(NewIssuesNotification.class, MyNewIssuesNotification.class, IssueChangeNotification.class);
+ static final Set<Class<? extends Notification>> NOTIF_TYPES = ImmutableSet.of(NewIssuesNotification.class, MyNewIssuesNotification.class, IssuesChangesNotification.class);
private final IssueCache issueCache;
- private final RuleRepository rules;
private final TreeRootHolder treeRootHolder;
private final NotificationService service;
private final AnalysisMetadataHolder analysisMetadataHolder;
- private final NewIssuesNotificationFactory newIssuesNotificationFactory;
+ private final NotificationFactory notificationFactory;
private final DbClient dbClient;
private Map<String, Component> componentsByDbKey;
- public SendIssueNotificationsStep(IssueCache issueCache, RuleRepository rules, TreeRootHolder treeRootHolder,
+ public SendIssueNotificationsStep(IssueCache issueCache, TreeRootHolder treeRootHolder,
NotificationService service, AnalysisMetadataHolder analysisMetadataHolder,
- NewIssuesNotificationFactory newIssuesNotificationFactory, DbClient dbClient) {
+ NotificationFactory notificationFactory, DbClient dbClient) {
this.issueCache = issueCache;
- this.rules = rules;
this.treeRootHolder = treeRootHolder;
this.service = service;
this.analysisMetadataHolder = analysisMetadataHolder;
- this.newIssuesNotificationFactory = newIssuesNotificationFactory;
+ this.notificationFactory = notificationFactory;
this.dbClient = dbClient;
}
@@ -125,12 +115,12 @@ public class SendIssueNotificationsStep implements ComputationStep {
Map<String, UserDto> assigneesByUuid;
try (DbSession dbSession = dbClient.openSession(false)) {
Iterable<DefaultIssue> iterable = issueCache::traverse;
- Set<String> assigneeUuids = stream(iterable.spliterator(), false).map(DefaultIssue::assignee).filter(Objects::nonNull).collect(toSet());
+ Set<String> assigneeUuids = stream(iterable.spliterator(), false).map(DefaultIssue::assignee).filter(Objects::nonNull).collect(Collectors.toSet());
assigneesByUuid = dbClient.userDao().selectByUuids(dbSession, assigneeUuids).stream().collect(toMap(UserDto::getUuid, dto -> dto));
}
try (CloseableIterator<DefaultIssue> issues = issueCache.traverse()) {
- processIssues(newIssuesStats, issues, project, assigneesByUuid, notificationStatistics);
+ processIssues(newIssuesStats, issues, assigneesByUuid, notificationStatistics);
}
if (newIssuesStats.hasIssuesOnLeak()) {
sendNewIssuesNotification(newIssuesStats, project, assigneesByUuid, analysisDate, notificationStatistics);
@@ -148,56 +138,45 @@ public class SendIssueNotificationsStep implements ComputationStep {
return Date.from(instant).getTime();
}
- private void processIssues(NewIssuesStatistics newIssuesStats, CloseableIterator<DefaultIssue> issues, Component project, Map<String, UserDto> usersDtoByUuids,
- NotificationStatistics notificationStatistics) {
+ private void processIssues(NewIssuesStatistics newIssuesStats, CloseableIterator<DefaultIssue> issues,
+ Map<String, UserDto> assigneesByUuid, NotificationStatistics notificationStatistics) {
int batchSize = 1000;
- List<DefaultIssue> loadedIssues = new ArrayList<>(batchSize);
+ Set<DefaultIssue> changedIssuesToNotify = new HashSet<>(batchSize);
while (issues.hasNext()) {
DefaultIssue issue = issues.next();
if (issue.type() != RuleType.SECURITY_HOTSPOT) {
if (issue.isNew() && issue.resolution() == null) {
newIssuesStats.add(issue);
} else if (issue.isChanged() && issue.mustSendNotifications()) {
- loadedIssues.add(issue);
+ changedIssuesToNotify.add(issue);
}
}
- if (loadedIssues.size() >= batchSize) {
- sendIssueChangeNotification(loadedIssues, project, usersDtoByUuids, notificationStatistics);
- loadedIssues.clear();
+ if (changedIssuesToNotify.size() >= batchSize) {
+ sendIssuesChangesNotification(changedIssuesToNotify, assigneesByUuid, notificationStatistics);
+ changedIssuesToNotify.clear();
}
}
- if (!loadedIssues.isEmpty()) {
- sendIssueChangeNotification(loadedIssues, project, usersDtoByUuids, notificationStatistics);
+ if (!changedIssuesToNotify.isEmpty()) {
+ sendIssuesChangesNotification(changedIssuesToNotify, assigneesByUuid, notificationStatistics);
}
}
- private void sendIssueChangeNotification(Collection<DefaultIssue> issues, Component project, Map<String, UserDto> usersDtoByUuids,
- NotificationStatistics notificationStatistics) {
- Set<IssueChangeNotification> notifications = issues.stream()
- .map(issue -> {
- IssueChangeNotification notification = new IssueChangeNotification();
- notification.setRuleName(rules.getByKey(issue.ruleKey()).getName());
- notification.setIssue(issue);
- notification.setAssignee(usersDtoByUuids.get(issue.assignee()));
- notification.setProject(project.getKey(), project.getName(), getBranchName(), getPullRequest());
- getComponentKey(issue).ifPresent(c -> notification.setComponent(c.getKey(), c.getName()));
- return notification;
- })
- .collect(MoreCollectors.toSet(issues.size()));
+ private void sendIssuesChangesNotification(Set<DefaultIssue> issues, Map<String, UserDto> assigneesByUuid, NotificationStatistics notificationStatistics) {
+ IssuesChangesNotification notification = notificationFactory.newIssuesChangesNotification(issues, assigneesByUuid);
- notificationStatistics.issueChangesDeliveries += service.deliverEmails(notifications);
+ notificationStatistics.issueChangesDeliveries += service.deliverEmails(singleton(notification));
notificationStatistics.issueChanges++;
// compatibility with old API
- notifications.forEach(notification -> notificationStatistics.issueChangesDeliveries += service.deliver(notification));
+ notificationStatistics.issueChangesDeliveries += service.deliver(notification);
}
private void sendNewIssuesNotification(NewIssuesStatistics statistics, Component project, Map<String, UserDto> assigneesByUuid,
long analysisDate, NotificationStatistics notificationStatistics) {
NewIssuesStatistics.Stats globalStatistics = statistics.globalStatistics();
- NewIssuesNotification notification = newIssuesNotificationFactory
+ NewIssuesNotification notification = notificationFactory
.newNewIssuesNotification(assigneesByUuid)
.setProject(project.getKey(), project.getName(), getBranchName(), getPullRequest())
.setProjectVersion(project.getProjectAttributes().getProjectVersion())
@@ -220,7 +199,7 @@ public class SendIssueNotificationsStep implements ComputationStep {
.map(e -> {
String assigneeUuid = e.getKey();
NewIssuesStatistics.Stats assigneeStatistics = e.getValue();
- MyNewIssuesNotification myNewIssuesNotification = newIssuesNotificationFactory
+ MyNewIssuesNotification myNewIssuesNotification = notificationFactory
.newMyNewIssuesNotification(assigneesByUuid)
.setAssignee(userDtoByUuid.get(assigneeUuid));
myNewIssuesNotification
@@ -232,7 +211,7 @@ public class SendIssueNotificationsStep implements ComputationStep {
return myNewIssuesNotification;
})
- .collect(MoreCollectors.toSet(statistics.getAssigneesStatistics().size()));
+ .collect(toSet(statistics.getAssigneesStatistics().size()));
notificationStatistics.myNewIssuesDeliveries += service.deliverEmails(myNewIssuesNotifications);
notificationStatistics.myNewIssues += myNewIssuesNotifications.size();
@@ -251,21 +230,6 @@ public class SendIssueNotificationsStep implements ComputationStep {
}
}
- private Optional<Component> getComponentKey(DefaultIssue issue) {
- if (componentsByDbKey == null) {
- final ImmutableMap.Builder<String, Component> builder = ImmutableMap.builder();
- new DepthTraversalTypeAwareCrawler(
- new TypeAwareVisitorAdapter(CrawlerDepthLimit.LEAVES, POST_ORDER) {
- @Override
- public void visitAny(Component component) {
- builder.put(component.getDbKey(), component);
- }
- }).visit(this.treeRootHolder.getRoot());
- this.componentsByDbKey = builder.build();
- }
- return Optional.ofNullable(componentsByDbKey.get(issue.componentKey()));
- }
-
@Override
public String getDescription() {
return "Send issue notifications";
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/DumbRule.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/DumbRule.java
index 9861faf598f..95e0bb75104 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/DumbRule.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/DumbRule.java
@@ -46,6 +46,7 @@ public class DumbRule implements Rule {
public DumbRule(RuleKey key) {
this.key = key;
this.id = key.hashCode();
+ this.name = "name_" + key;
}
@Override
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/notification/NewIssuesNotificationFactoryTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/notification/NotificationFactoryTest.java
index 37a9210253f..fe61a0238f3 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/notification/NewIssuesNotificationFactoryTest.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/notification/NotificationFactoryTest.java
@@ -20,7 +20,13 @@
package org.sonar.ce.task.projectanalysis.notification;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
@@ -29,36 +35,58 @@ import java.util.stream.Stream;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.utils.Durations;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule;
+import org.sonar.ce.task.projectanalysis.analysis.Branch;
import org.sonar.ce.task.projectanalysis.component.ReportComponent;
import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
import org.sonar.ce.task.projectanalysis.issue.DumbRule;
import org.sonar.ce.task.projectanalysis.issue.RuleRepositoryRule;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.db.component.BranchType;
import org.sonar.db.user.UserDto;
import org.sonar.db.user.UserTesting;
+import org.sonar.server.issue.notification.IssuesChangesNotification;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer;
import org.sonar.server.issue.notification.MyNewIssuesNotification;
import org.sonar.server.issue.notification.NewIssuesNotification;
import org.sonar.server.issue.notification.NewIssuesNotification.DetailsSupplier;
import org.sonar.server.issue.notification.NewIssuesNotification.RuleDefinition;
import static java.util.Collections.emptyMap;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.issue.Issue.STATUS_OPEN;
import static org.sonar.ce.task.projectanalysis.component.Component.Type.DIRECTORY;
import static org.sonar.ce.task.projectanalysis.component.Component.Type.FILE;
import static org.sonar.ce.task.projectanalysis.component.Component.Type.PROJECT;
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
-public class NewIssuesNotificationFactoryTest {
+@RunWith(DataProviderRunner.class)
+public class NotificationFactoryTest {
@Rule
public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
@Rule
public RuleRepositoryRule ruleRepository = new RuleRepositoryRule();
@Rule
+ public AnalysisMetadataHolderRule analysisMetadata = new AnalysisMetadataHolderRule();
+ @Rule
public ExpectedException expectedException = ExpectedException.none();
private Durations durations = new Durations();
- private NewIssuesNotificationFactory underTest = new NewIssuesNotificationFactory(treeRootHolder, ruleRepository, durations);
+ private IssuesChangesNotificationSerializer issuesChangesSerializer = mock(IssuesChangesNotificationSerializer.class);
+ private NotificationFactory underTest = new NotificationFactory(treeRootHolder, analysisMetadata, ruleRepository, durations, issuesChangesSerializer);
@Test
public void newMyNewIssuesNotification_throws_NPE_if_assigneesByUuid_is_null() {
@@ -392,6 +420,375 @@ public class NewIssuesNotificationFactoryTest {
.isEmpty();
}
+ @Test
+ public void newIssuesChangesNotification_fails_with_ISE_if_analysis_date_has_not_been_set() {
+ Set<DefaultIssue> issues = IntStream.range(0, 1 + new Random().nextInt(2))
+ .mapToObj(i -> new DefaultIssue())
+ .collect(Collectors.toSet());
+ Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid();
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Analysis date has not been set");
+
+ underTest.newIssuesChangesNotification(issues, assigneesByUuid);
+ }
+
+ @Test
+ public void newIssuesChangesNotification_fails_with_IAE_if_issues_is_empty() {
+ analysisMetadata.setAnalysisDate(new Random().nextLong());
+ Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid();
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("issues can't be empty");
+
+ underTest.newIssuesChangesNotification(Collections.emptySet(), assigneesByUuid);
+ }
+
+ @Test
+ public void newIssuesChangesNotification_fails_with_NPE_if_issue_has_no_rule() {
+ DefaultIssue issue = new DefaultIssue();
+ Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid();
+ analysisMetadata.setAnalysisDate(new Random().nextLong());
+
+ expectedException.expect(NullPointerException.class);
+
+ underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid);
+ }
+
+ @Test
+ public void newIssuesChangesNotification_fails_with_ISE_if_rule_of_issue_does_not_exist_in_repository() {
+ RuleKey ruleKey = RuleKey.of("foo", "bar");
+ DefaultIssue issue = new DefaultIssue()
+ .setRuleKey(ruleKey);
+ Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid();
+ analysisMetadata.setAnalysisDate(new Random().nextLong());
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Can not find rule " + ruleKey + " in RuleRepository");
+
+ underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid);
+ }
+
+ @Test
+ public void newIssuesChangesNotification_fails_with_ISE_if_treeRootHolder_is_empty() {
+ RuleKey ruleKey = RuleKey.of("foo", "bar");
+ DefaultIssue issue = new DefaultIssue()
+ .setRuleKey(ruleKey);
+ Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid();
+ ruleRepository.add(ruleKey);
+ analysisMetadata.setAnalysisDate(new Random().nextLong());
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Holder has not been initialized yet");
+
+ underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid);
+ }
+
+ @Test
+ public void newIssuesChangesNotification_fails_with_ISE_if_branch_has_not_been_set() {
+ RuleKey ruleKey = RuleKey.of("foo", "bar");
+ DefaultIssue issue = new DefaultIssue()
+ .setRuleKey(ruleKey);
+ Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid();
+ ruleRepository.add(ruleKey);
+ analysisMetadata.setAnalysisDate(new Random().nextLong());
+ treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).build());
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Branch has not been set");
+
+ underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid);
+ }
+
+ @Test
+ public void newIssuesChangesNotification_fails_with_NPE_if_issue_has_no_key() {
+ RuleKey ruleKey = RuleKey.of("foo", "bar");
+ DefaultIssue issue = new DefaultIssue()
+ .setRuleKey(ruleKey);
+ Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid();
+ ruleRepository.add(ruleKey);
+ treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).build());
+ analysisMetadata.setAnalysisDate(new Random().nextLong());
+ analysisMetadata.setBranch(mock(Branch.class));
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("key can't be null");
+
+ underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid);
+ }
+
+ @Test
+ public void newIssuesChangesNotification_fails_with_NPE_if_issue_has_no_status() {
+ RuleKey ruleKey = RuleKey.of("foo", "bar");
+ DefaultIssue issue = new DefaultIssue()
+ .setRuleKey(ruleKey)
+ .setKey("issueKey");
+ Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid();
+ ruleRepository.add(ruleKey);
+ treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).build());
+ analysisMetadata.setAnalysisDate(new Random().nextLong());
+ analysisMetadata.setBranch(mock(Branch.class));
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("newStatus can't be null");
+
+ underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid);
+ }
+
+ @Test
+ @UseDataProvider("noBranchNameBranches")
+ public void newIssuesChangesNotification_creates_project_from_TreeRootHolder_and_branch_name_only_on_long_non_main_branches(Branch branch) {
+ RuleKey ruleKey = RuleKey.of("foo", "bar");
+ DefaultIssue issue = new DefaultIssue()
+ .setRuleKey(ruleKey)
+ .setKey("issueKey")
+ .setStatus(STATUS_OPEN);
+ Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid();
+ ReportComponent project = ReportComponent.builder(PROJECT, 1).build();
+ ruleRepository.add(ruleKey);
+ treeRootHolder.setRoot(project);
+ analysisMetadata.setAnalysisDate(new Random().nextLong());
+ analysisMetadata.setBranch(branch);
+ IssuesChangesNotification expected = mock(IssuesChangesNotification.class);
+ when(issuesChangesSerializer.serialize(any(IssuesChangesNotificationBuilder.class))).thenReturn(expected);
+
+ IssuesChangesNotification notification = underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid);
+
+ assertThat(notification).isSameAs(expected);
+
+ IssuesChangesNotificationBuilder builder = verifyAndCaptureIssueChangeNotificationBuilder();
+ assertThat(builder.getIssues()).hasSize(1);
+ ChangedIssue changeIssue = builder.getIssues().iterator().next();
+ assertThat(changeIssue.getProject().getUuid()).isEqualTo(project.getUuid());
+ assertThat(changeIssue.getProject().getKey()).isEqualTo(project.getKey());
+ assertThat(changeIssue.getProject().getProjectName()).isEqualTo(project.getName());
+ assertThat(changeIssue.getProject().getBranchName()).isEmpty();
+ }
+
+ @DataProvider
+ public static Object[][] noBranchNameBranches() {
+ Branch mainBranch = mock(Branch.class);
+ when(mainBranch.isMain()).thenReturn(true);
+ when(mainBranch.isLegacyFeature()).thenReturn(false);
+ when(mainBranch.getType()).thenReturn(BranchType.LONG);
+ Branch legacyBranch = mock(Branch.class);
+ when(legacyBranch.isLegacyFeature()).thenReturn(true);
+ Branch shortBranch = mock(Branch.class);
+ when(shortBranch.isLegacyFeature()).thenReturn(false);
+ when(shortBranch.isMain()).thenReturn(false);
+ when(shortBranch.getType()).thenReturn(BranchType.SHORT);
+ Branch pr = mock(Branch.class);
+ when(pr.isLegacyFeature()).thenReturn(false);
+ when(pr.isMain()).thenReturn(false);
+ when(pr.getType()).thenReturn(BranchType.PULL_REQUEST);
+ return new Object[][] {
+ {mainBranch},
+ {legacyBranch},
+ {shortBranch},
+ {pr}
+ };
+ }
+
+ @Test
+ public void newIssuesChangesNotification_creates_project_from_TreeRootHolder_and_branch_name_from_long_branch() {
+ RuleKey ruleKey = RuleKey.of("foo", "bar");
+ DefaultIssue issue = new DefaultIssue()
+ .setRuleKey(ruleKey)
+ .setKey("issueKey")
+ .setStatus(STATUS_OPEN);
+ Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid();
+ ReportComponent project = ReportComponent.builder(PROJECT, 1).build();
+ String branchName = randomAlphabetic(12);
+ ruleRepository.add(ruleKey);
+ treeRootHolder.setRoot(project);
+ analysisMetadata.setAnalysisDate(new Random().nextLong());
+ analysisMetadata.setBranch(newBranch(BranchType.LONG, branchName));
+ IssuesChangesNotification expected = mock(IssuesChangesNotification.class);
+ when(issuesChangesSerializer.serialize(any(IssuesChangesNotificationBuilder.class))).thenReturn(expected);
+
+ IssuesChangesNotification notification = underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid);
+
+ assertThat(notification).isSameAs(expected);
+
+ IssuesChangesNotificationBuilder builder = verifyAndCaptureIssueChangeNotificationBuilder();
+ assertThat(builder.getIssues()).hasSize(1);
+ ChangedIssue changeIssue = builder.getIssues().iterator().next();
+ assertThat(changeIssue.getProject().getUuid()).isEqualTo(project.getUuid());
+ assertThat(changeIssue.getProject().getKey()).isEqualTo(project.getKey());
+ assertThat(changeIssue.getProject().getProjectName()).isEqualTo(project.getName());
+ assertThat(changeIssue.getProject().getBranchName()).contains(branchName);
+ }
+
+ @Test
+ public void newIssuesChangesNotification_creates_rule_from_RuleRepository() {
+ RuleKey ruleKey = RuleKey.of("foo", "bar");
+ DefaultIssue issue = new DefaultIssue()
+ .setRuleKey(ruleKey)
+ .setKey("issueKey")
+ .setStatus(STATUS_OPEN);
+ Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid();
+ ReportComponent project = ReportComponent.builder(PROJECT, 1).build();
+ String branchName = randomAlphabetic(12);
+ ruleRepository.add(ruleKey);
+ treeRootHolder.setRoot(project);
+ analysisMetadata.setAnalysisDate(new Random().nextLong());
+ analysisMetadata.setBranch(newBranch(BranchType.LONG, branchName));
+ IssuesChangesNotification expected = mock(IssuesChangesNotification.class);
+ when(issuesChangesSerializer.serialize(any(IssuesChangesNotificationBuilder.class))).thenReturn(expected);
+
+ IssuesChangesNotification notification = underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid);
+
+ assertThat(notification).isSameAs(expected);
+ IssuesChangesNotificationBuilder builder = verifyAndCaptureIssueChangeNotificationBuilder();
+ assertThat(builder.getIssues()).hasSize(1);
+ ChangedIssue changeIssue = builder.getIssues().iterator().next();
+ assertThat(changeIssue.getRule().getKey()).isEqualTo(ruleKey);
+ assertThat(changeIssue.getRule().getName()).isEqualTo(ruleRepository.getByKey(ruleKey).getName());
+ }
+
+ @Test
+ public void newIssuesChangesNotification_fails_with_ISE_if_issue_has_assignee_not_in_assigneesByUuid() {
+ RuleKey ruleKey = RuleKey.of("foo", "bar");
+ String assigneeUuid = randomAlphabetic(40);
+ DefaultIssue issue = new DefaultIssue()
+ .setRuleKey(ruleKey)
+ .setKey("issueKey")
+ .setStatus(STATUS_OPEN)
+ .setAssigneeUuid(assigneeUuid);
+ Map<String, UserDto> assigneesByUuid = Collections.emptyMap();
+ ReportComponent project = ReportComponent.builder(PROJECT, 1).build();
+ ruleRepository.add(ruleKey);
+ treeRootHolder.setRoot(project);
+ analysisMetadata.setAnalysisDate(new Random().nextLong());
+ analysisMetadata.setBranch(newBranch(BranchType.LONG, randomAlphabetic(12)));
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Can not find DTO for assignee uuid " + assigneeUuid);
+
+ underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid);
+ }
+
+ @Test
+ public void newIssuesChangesNotification_creates_assignee_from_UserDto() {
+ RuleKey ruleKey = RuleKey.of("foo", "bar");
+ String assigneeUuid = randomAlphabetic(40);
+ DefaultIssue issue = new DefaultIssue()
+ .setRuleKey(ruleKey)
+ .setKey("issueKey")
+ .setStatus(STATUS_OPEN)
+ .setAssigneeUuid(assigneeUuid);
+ UserDto userDto = UserTesting.newUserDto();
+ Map<String, UserDto> assigneesByUuid = ImmutableMap.of(assigneeUuid, userDto);
+ ReportComponent project = ReportComponent.builder(PROJECT, 1).build();
+ ruleRepository.add(ruleKey);
+ treeRootHolder.setRoot(project);
+ analysisMetadata.setAnalysisDate(new Random().nextLong());
+ analysisMetadata.setBranch(newBranch(BranchType.LONG, randomAlphabetic(12)));
+ IssuesChangesNotification expected = mock(IssuesChangesNotification.class);
+ when(issuesChangesSerializer.serialize(any(IssuesChangesNotificationBuilder.class))).thenReturn(expected);
+
+ IssuesChangesNotification notification = underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid);
+
+ assertThat(notification).isSameAs(expected);
+ IssuesChangesNotificationBuilder builder = verifyAndCaptureIssueChangeNotificationBuilder();
+ assertThat(builder.getIssues()).hasSize(1);
+ ChangedIssue changeIssue = builder.getIssues().iterator().next();
+ assertThat(changeIssue.getAssignee()).isPresent();
+ IssuesChangesNotificationBuilder.User assignee = changeIssue.getAssignee().get();
+ assertThat(assignee.getUuid()).isEqualTo(userDto.getUuid());
+ assertThat(assignee.getName()).contains(userDto.getName());
+ assertThat(assignee.getLogin()).isEqualTo(userDto.getLogin());
+ }
+
+ @Test
+ public void newIssuesChangesNotification_creates_AnalysisChange_with_analysis_date() {
+ RuleKey ruleKey = RuleKey.of("foo", "bar");
+ DefaultIssue issue = new DefaultIssue()
+ .setRuleKey(ruleKey)
+ .setKey("issueKey")
+ .setStatus(STATUS_OPEN);
+ Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid();
+ ReportComponent project = ReportComponent.builder(PROJECT, 1).build();
+ long analysisDate = new Random().nextLong();
+ ruleRepository.add(ruleKey);
+ treeRootHolder.setRoot(project);
+ analysisMetadata.setAnalysisDate(analysisDate);
+ analysisMetadata.setBranch(newBranch(BranchType.LONG, randomAlphabetic(12)));
+ IssuesChangesNotification expected = mock(IssuesChangesNotification.class);
+ when(issuesChangesSerializer.serialize(any(IssuesChangesNotificationBuilder.class))).thenReturn(expected);
+
+ IssuesChangesNotification notification = underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid);
+
+ assertThat(notification).isSameAs(expected);
+ IssuesChangesNotificationBuilder builder = verifyAndCaptureIssueChangeNotificationBuilder();
+ assertThat(builder.getIssues()).hasSize(1);
+ assertThat(builder.getChange())
+ .isInstanceOf(AnalysisChange.class)
+ .extracting(IssuesChangesNotificationBuilder.Change::getDate)
+ .containsOnly(analysisDate);
+ }
+
+ @Test
+ public void newIssuesChangesNotification_maps_all_issues() {
+ Set<DefaultIssue> issues = IntStream.range(0, 3 + new Random().nextInt(5))
+ .mapToObj(i -> new DefaultIssue()
+ .setRuleKey(RuleKey.of("repo_" + i, "rule_" + i))
+ .setKey("issue_key_" + i)
+ .setStatus("status_" + i))
+ .collect(Collectors.toSet());
+ ReportComponent project = ReportComponent.builder(PROJECT, 1).build();
+ long analysisDate = new Random().nextLong();
+ issues.stream()
+ .map(DefaultIssue::ruleKey)
+ .forEach(ruleKey -> ruleRepository.add(ruleKey));
+ treeRootHolder.setRoot(project);
+ analysisMetadata.setAnalysisDate(analysisDate);
+ analysisMetadata.setBranch(newBranch(BranchType.LONG, randomAlphabetic(12)));
+ IssuesChangesNotification expected = mock(IssuesChangesNotification.class);
+ when(issuesChangesSerializer.serialize(any(IssuesChangesNotificationBuilder.class))).thenReturn(expected);
+
+ IssuesChangesNotification notification = underTest.newIssuesChangesNotification(issues, emptyMap());
+
+ assertThat(notification).isSameAs(expected);
+ IssuesChangesNotificationBuilder builder = verifyAndCaptureIssueChangeNotificationBuilder();
+ assertThat(builder.getIssues()).hasSize(issues.size());
+ Map<String, ChangedIssue> changedIssuesByKey = builder.getIssues().stream()
+ .collect(uniqueIndex(ChangedIssue::getKey));
+ issues.forEach(
+ issue -> {
+ ChangedIssue changedIssue = changedIssuesByKey.get(issue.key());
+ assertThat(changedIssue.getNewStatus()).isEqualTo(issue.status());
+ assertThat(changedIssue.getNewResolution()).isEmpty();
+ assertThat(changedIssue.getAssignee()).isEmpty();
+ assertThat(changedIssue.getRule().getKey()).isEqualTo(issue.ruleKey());
+ assertThat(changedIssue.getRule().getName()).isEqualTo(ruleRepository.getByKey(issue.ruleKey()).getName());
+ }
+ );
+ }
+
+ private static Map<String, UserDto> nonEmptyAssigneesByUuid() {
+ return IntStream.range(0, 1 + new Random().nextInt(3))
+ .boxed()
+ .collect(uniqueIndex(i -> "uuid_" + i, i -> new UserDto()));
+ }
+
+ private IssuesChangesNotificationBuilder verifyAndCaptureIssueChangeNotificationBuilder() {
+ ArgumentCaptor<IssuesChangesNotificationBuilder> builderCaptor = ArgumentCaptor.forClass(IssuesChangesNotificationBuilder.class);
+ verify(issuesChangesSerializer).serialize(builderCaptor.capture());
+ verifyNoMoreInteractions(issuesChangesSerializer);
+
+ return builderCaptor.getValue();
+ }
+
+ private static Branch newBranch(BranchType branchType, String branchName) {
+ Branch longBranch = mock(Branch.class);
+ when(longBranch.isLegacyFeature()).thenReturn(false);
+ when(longBranch.isMain()).thenReturn(false);
+ when(longBranch.getType()).thenReturn(branchType);
+ when(longBranch.getName()).thenReturn(branchName);
+ return longBranch;
+ }
+
private static Durations readDurationsField(NewIssuesNotification notification) {
return readField(notification, "durations");
}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStepTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStepTest.java
index 22dc5ac1ede..6d077e53d34 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStepTest.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStepTest.java
@@ -19,14 +19,18 @@
*/
package org.sonar.ce.task.projectanalysis.step;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
+import java.util.Set;
+import java.util.function.Supplier;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.assertj.core.groups.Tuple;
@@ -35,6 +39,8 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.ArgumentCaptor;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
import org.sonar.api.notifications.Notification;
import org.sonar.api.rules.RuleType;
import org.sonar.api.utils.Duration;
@@ -45,8 +51,7 @@ import org.sonar.ce.task.projectanalysis.component.Component;
import org.sonar.ce.task.projectanalysis.component.DefaultBranchImpl;
import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
import org.sonar.ce.task.projectanalysis.issue.IssueCache;
-import org.sonar.ce.task.projectanalysis.issue.RuleRepositoryRule;
-import org.sonar.ce.task.projectanalysis.notification.NewIssuesNotificationFactory;
+import org.sonar.ce.task.projectanalysis.notification.NotificationFactory;
import org.sonar.ce.task.projectanalysis.util.cache.DiskCache;
import org.sonar.ce.task.step.ComputationStep;
import org.sonar.ce.task.step.TestComputationStepContext;
@@ -57,7 +62,7 @@ import org.sonar.db.component.ComponentDto;
import org.sonar.db.rule.RuleDefinitionDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.issue.notification.DistributedMetricStatsInt;
-import org.sonar.server.issue.notification.IssueChangeNotification;
+import org.sonar.server.issue.notification.IssuesChangesNotification;
import org.sonar.server.issue.notification.MyNewIssuesNotification;
import org.sonar.server.issue.notification.NewIssuesNotification;
import org.sonar.server.issue.notification.NewIssuesStatistics;
@@ -67,6 +72,7 @@ import org.sonar.server.project.Project;
import static java.util.Arrays.stream;
import static java.util.Collections.emptyList;
import static java.util.Collections.shuffle;
+import static java.util.Collections.singleton;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Stream.concat;
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
@@ -75,6 +81,8 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.groups.Tuple.tuple;
import static org.mockito.ArgumentCaptor.forClass;
import static org.mockito.ArgumentMatchers.anyCollection;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
@@ -121,8 +129,6 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
.setBranch(new DefaultBranchImpl())
.setAnalysisDate(new Date(ANALYSE_DATE));
@Rule
- public RuleRepositoryRule ruleRepository = new RuleRepositoryRule();
- @Rule
public TemporaryFolder temp = new TemporaryFolder();
@Rule
public DbTester db = DbTester.create(System2.INSTANCE);
@@ -132,9 +138,15 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
private final RuleType randomRuleType = RULE_TYPES_EXCEPT_HOTSPOTS[random.nextInt(RULE_TYPES_EXCEPT_HOTSPOTS.length)];
@SuppressWarnings("unchecked")
private Class<Map<String, UserDto>> assigneeCacheType = (Class<Map<String, UserDto>>) (Object) Map.class;
+ @SuppressWarnings("unchecked")
+ private Class<Set<DefaultIssue>> setType = (Class<Set<DefaultIssue>>) (Class<?>) Set.class;
+ @SuppressWarnings("unchecked")
+ private Class<Map<String, UserDto>> mapType = (Class<Map<String, UserDto>>) (Class<?>) Map.class;
private ArgumentCaptor<Map<String, UserDto>> assigneeCacheCaptor = ArgumentCaptor.forClass(assigneeCacheType);
+ private ArgumentCaptor<Set<DefaultIssue>> issuesSetCaptor = forClass(setType);
+ private ArgumentCaptor<Map<String, UserDto>> assigneeByUuidCaptor = forClass(mapType);
private NotificationService notificationService = mock(NotificationService.class);
- private NewIssuesNotificationFactory newIssuesNotificationFactory = mock(NewIssuesNotificationFactory.class);
+ private NotificationFactory notificationFactory = mock(NotificationFactory.class);
private NewIssuesNotification newIssuesNotificationMock = createNewIssuesNotificationMock();
private MyNewIssuesNotification myNewIssuesNotificationMock = createMyNewIssuesNotificationMock();
@@ -144,10 +156,10 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
@Before
public void setUp() throws Exception {
issueCache = new IssueCache(temp.newFile(), System2.INSTANCE);
- underTest = new SendIssueNotificationsStep(issueCache, ruleRepository, treeRootHolder, notificationService, analysisMetadataHolder,
- newIssuesNotificationFactory, db.getDbClient());
- when(newIssuesNotificationFactory.newNewIssuesNotification(any(assigneeCacheType))).thenReturn(newIssuesNotificationMock);
- when(newIssuesNotificationFactory.newMyNewIssuesNotification(any(assigneeCacheType))).thenReturn(myNewIssuesNotificationMock);
+ underTest = new SendIssueNotificationsStep(issueCache, treeRootHolder, notificationService, analysisMetadataHolder,
+ notificationFactory, db.getDbClient());
+ when(notificationFactory.newNewIssuesNotification(any(assigneeCacheType))).thenReturn(newIssuesNotificationMock);
+ when(notificationFactory.newMyNewIssuesNotification(any(assigneeCacheType))).thenReturn(myNewIssuesNotificationMock);
}
@Test
@@ -360,19 +372,19 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList()));
when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
- NewIssuesNotificationFactory newIssuesNotificationFactory = mock(NewIssuesNotificationFactory.class);
+ NotificationFactory notificationFactory = mock(NotificationFactory.class);
NewIssuesNotification newIssuesNotificationMock = createNewIssuesNotificationMock();
- when(newIssuesNotificationFactory.newNewIssuesNotification(assigneeCacheCaptor.capture()))
+ when(notificationFactory.newNewIssuesNotification(assigneeCacheCaptor.capture()))
.thenReturn(newIssuesNotificationMock);
MyNewIssuesNotification myNewIssuesNotificationMock1 = createMyNewIssuesNotificationMock();
MyNewIssuesNotification myNewIssuesNotificationMock2 = createMyNewIssuesNotificationMock();
- when(newIssuesNotificationFactory.newMyNewIssuesNotification(any(assigneeCacheType)))
+ when(notificationFactory.newMyNewIssuesNotification(any(assigneeCacheType)))
.thenReturn(myNewIssuesNotificationMock1)
.thenReturn(myNewIssuesNotificationMock2);
TestComputationStepContext context = new TestComputationStepContext();
- new SendIssueNotificationsStep(issueCache, ruleRepository, treeRootHolder, notificationService, analysisMetadataHolder, newIssuesNotificationFactory, db.getDbClient())
+ new SendIssueNotificationsStep(issueCache, treeRootHolder, notificationService, analysisMetadataHolder, notificationFactory, db.getDbClient())
.execute(context);
verify(notificationService).deliverEmails(ImmutableSet.of(myNewIssuesNotificationMock1, myNewIssuesNotificationMock2));
@@ -380,9 +392,9 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
verify(notificationService).deliver(myNewIssuesNotificationMock1);
verify(notificationService).deliver(myNewIssuesNotificationMock2);
- verify(newIssuesNotificationFactory).newNewIssuesNotification(assigneeCacheCaptor.capture());
- verify(newIssuesNotificationFactory, times(2)).newMyNewIssuesNotification(assigneeCacheCaptor.capture());
- verifyNoMoreInteractions(newIssuesNotificationFactory);
+ verify(notificationFactory).newNewIssuesNotification(assigneeCacheCaptor.capture());
+ verify(notificationFactory, times(2)).newMyNewIssuesNotification(assigneeCacheCaptor.capture());
+ verifyNoMoreInteractions(notificationFactory);
verifyAssigneeCache(assigneeCacheCaptor, perceval, arthur);
Map<String, MyNewIssuesNotification> myNewIssuesNotificationMocksByUsersName = new HashMap<>();
@@ -439,10 +451,9 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
// old API compatibility
verify(notificationService).deliver(myNewIssuesNotificationMock);
-
- verify(newIssuesNotificationFactory).newNewIssuesNotification(assigneeCacheCaptor.capture());
- verify(newIssuesNotificationFactory).newMyNewIssuesNotification(assigneeCacheCaptor.capture());
- verifyNoMoreInteractions(newIssuesNotificationFactory);
+ verify(notificationFactory).newNewIssuesNotification(assigneeCacheCaptor.capture());
+ verify(notificationFactory).newMyNewIssuesNotification(assigneeCacheCaptor.capture());
+ verifyNoMoreInteractions(notificationFactory);
verifyAssigneeCache(assigneeCacheCaptor, user);
verify(myNewIssuesNotificationMock).setAssignee(any(UserDto.class));
@@ -499,7 +510,7 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
ComponentDto project = newPrivateProjectDto(newOrganizationDto()).setDbKey(PROJECT.getDbKey()).setLongName(PROJECT.getName());
ComponentDto file = newFileDto(project).setDbKey(FILE.getDbKey()).setLongName(FILE.getName());
RuleDefinitionDto ruleDefinitionDto = newRule();
- DefaultIssue issue = prepareIssue(ANALYSE_DATE, user, project, file, ruleDefinitionDto, RuleType.SECURITY_HOTSPOT);
+ prepareIssue(ANALYSE_DATE, user, project, file, ruleDefinitionDto, RuleType.SECURITY_HOTSPOT);
analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList()));
when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
@@ -521,29 +532,33 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
ComponentDto project = newPrivateProjectDto(newOrganizationDto()).setDbKey(PROJECT.getDbKey()).setLongName(PROJECT.getName());
analysisMetadataHolder.setProject(Project.from(project));
ComponentDto file = newFileDto(project).setDbKey(FILE.getDbKey()).setLongName(FILE.getName());
+ treeRootHolder.setRoot(builder(Type.PROJECT, 2).setKey(project.getDbKey()).setPublicKey(project.getKey()).setName(project.longName()).setUuid(project.uuid())
+ .addChildren(
+ builder(Type.FILE, 11).setKey(file.getDbKey()).setPublicKey(file.getKey()).setName(file.longName()).build())
+ .build());
RuleDefinitionDto ruleDefinitionDto = newRule();
RuleType randomTypeExceptHotspot = RuleType.values()[nextInt(RuleType.values().length - 1)];
DefaultIssue issue = prepareIssue(issueCreatedAt, user, project, file, ruleDefinitionDto, randomTypeExceptHotspot);
+ IssuesChangesNotification issuesChangesNotification = mock(IssuesChangesNotification.class);
+ when(notificationService.hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES)).thenReturn(true);
+ when(notificationFactory.newIssuesChangesNotification(anySet(), anyMap())).thenReturn(issuesChangesNotification);
underTest.execute(new TestComputationStepContext());
- ArgumentCaptor<IssueChangeNotification> issueChangeNotificationCaptor = forClass(IssueChangeNotification.class);
- verify(notificationService).deliver(issueChangeNotificationCaptor.capture());
- IssueChangeNotification issueChangeNotification = issueChangeNotificationCaptor.getValue();
- assertThat(issueChangeNotification.getFieldValue("key")).isEqualTo(issue.key());
- assertThat(issueChangeNotification.getFieldValue("message")).isEqualTo(issue.message());
- assertThat(issueChangeNotification.getFieldValue("ruleName")).isEqualTo(ruleDefinitionDto.getName());
- assertThat(issueChangeNotification.getFieldValue("projectName")).isEqualTo(project.longName());
- assertThat(issueChangeNotification.getFieldValue("projectKey")).isEqualTo(project.getKey());
- assertThat(issueChangeNotification.getFieldValue("componentKey")).isEqualTo(file.getKey());
- assertThat(issueChangeNotification.getFieldValue("componentName")).isEqualTo(file.longName());
- assertThat(issueChangeNotification.getFieldValue("assignee")).isEqualTo(user.getLogin());
+ verify(notificationFactory).newIssuesChangesNotification(issuesSetCaptor.capture(), assigneeByUuidCaptor.capture());
+ assertThat(issuesSetCaptor.getValue()).hasSize(1);
+ assertThat(issuesSetCaptor.getValue().iterator().next()).isEqualTo(issue);
+ assertThat(assigneeByUuidCaptor.getValue()).hasSize(1);
+ assertThat(assigneeByUuidCaptor.getValue().get(user.getUuid())).isNotNull();
+ verify(notificationService).hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES);
+ verify(notificationService).deliverEmails(singleton(issuesChangesNotification));
+ verify(notificationService).deliver(issuesChangesNotification);
+ verifyNoMoreInteractions(notificationService);
}
private DefaultIssue prepareIssue(long issueCreatedAt, UserDto user, ComponentDto project, ComponentDto file, RuleDefinitionDto ruleDefinitionDto, RuleType type) {
DefaultIssue issue = newIssue(ruleDefinitionDto, project, file).setType(type).toDefaultIssue()
.setNew(false).setChanged(true).setSendNotifications(true).setCreationDate(new Date(issueCreatedAt)).setAssigneeUuid(user.getUuid());
- ruleRepository.add(ruleDefinitionDto.getKey()).setName(ruleDefinitionDto.getName());
issueCache.newAppender().append(issue).close();
when(notificationService.hasProjectSubscribersForTypes(project.projectUuid(), NOTIF_TYPES)).thenReturn(true);
return issue;
@@ -573,48 +588,85 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
.setChanged(true)
.setSendNotifications(true)
.setCreationDate(new Date(issueCreatedAt));
- ruleRepository.add(ruleDefinitionDto.getKey()).setName(ruleDefinitionDto.getName());
issueCache.newAppender().append(issue).close();
when(notificationService.hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES)).thenReturn(true);
+ IssuesChangesNotification issuesChangesNotification = mock(IssuesChangesNotification.class);
+ when(notificationFactory.newIssuesChangesNotification(anySet(), anyMap())).thenReturn(issuesChangesNotification);
analysisMetadataHolder.setBranch(newBranch(BranchType.LONG));
underTest.execute(new TestComputationStepContext());
- ArgumentCaptor<IssueChangeNotification> issueChangeNotificationCaptor = forClass(IssueChangeNotification.class);
- verify(notificationService).deliver(issueChangeNotificationCaptor.capture());
- IssueChangeNotification issueChangeNotification = issueChangeNotificationCaptor.getValue();
- assertThat(issueChangeNotification.getFieldValue("projectName")).isEqualTo(branch.longName());
- assertThat(issueChangeNotification.getFieldValue("projectKey")).isEqualTo(branch.getKey());
- assertThat(issueChangeNotification.getFieldValue("branch")).isEqualTo(BRANCH_NAME);
- assertThat(issueChangeNotification.getFieldValue("componentKey")).isEqualTo(file.getKey());
- assertThat(issueChangeNotification.getFieldValue("componentName")).isEqualTo(file.longName());
+ verify(notificationFactory).newIssuesChangesNotification(issuesSetCaptor.capture(), assigneeByUuidCaptor.capture());
+ assertThat(issuesSetCaptor.getValue()).hasSize(1);
+ assertThat(issuesSetCaptor.getValue().iterator().next()).isEqualTo(issue);
+ assertThat(assigneeByUuidCaptor.getValue()).isEmpty();
+ verify(notificationService).hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES);
+ verify(notificationService).deliverEmails(singleton(issuesChangesNotification));
+ verify(notificationService).deliver(issuesChangesNotification);
+ verifyNoMoreInteractions(notificationService);
}
@Test
- public void send_issue_change_notification_in_bulks_of_1000() {
+ public void sends_one_issue_change_notification_every_1000_issues() {
UserDto user = db.users().insertUser();
ComponentDto project = newPrivateProjectDto(newOrganizationDto()).setDbKey(PROJECT.getDbKey()).setLongName(PROJECT.getName());
ComponentDto file = newFileDto(project).setDbKey(FILE.getDbKey()).setLongName(FILE.getName());
RuleDefinitionDto ruleDefinitionDto = newRule();
- ruleRepository.add(ruleDefinitionDto.getKey()).setName(ruleDefinitionDto.getName());
RuleType randomTypeExceptHotspot = RuleType.values()[nextInt(RuleType.values().length - 1)];
- List<DefaultIssue> issues = IntStream.range(0, 1001 + new Random().nextInt(10))
- .mapToObj(i -> newIssue(ruleDefinitionDto, project, file).setType(randomTypeExceptHotspot).toDefaultIssue()
+ List<DefaultIssue> issues = IntStream.range(0, 2001 + new Random().nextInt(10))
+ .mapToObj(i -> newIssue(ruleDefinitionDto, project, file).setKee("uuid_" + i).setType(randomTypeExceptHotspot).toDefaultIssue()
.setNew(false).setChanged(true).setSendNotifications(true).setAssigneeUuid(user.getUuid()))
.collect(toList());
DiskCache<DefaultIssue>.DiskAppender diskAppender = issueCache.newAppender();
issues.forEach(diskAppender::append);
diskAppender.close();
analysisMetadataHolder.setProject(Project.from(project));
+ NewIssuesFactoryCaptor newIssuesFactoryCaptor = new NewIssuesFactoryCaptor(() -> mock(IssuesChangesNotification.class));
+ when(notificationFactory.newIssuesChangesNotification(anySet(), anyMap())).thenAnswer(newIssuesFactoryCaptor);
+ when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
when(notificationService.hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES)).thenReturn(true);
underTest.execute(new TestComputationStepContext());
+ verify(notificationFactory, times(3)).newIssuesChangesNotification(anySet(), anyMap());
+ assertThat(newIssuesFactoryCaptor.issuesSetCaptor).hasSize(3);
+ assertThat(newIssuesFactoryCaptor.issuesSetCaptor.get(0)).hasSize(1000);
+ assertThat(newIssuesFactoryCaptor.issuesSetCaptor.get(1)).hasSize(1000);
+ assertThat(newIssuesFactoryCaptor.issuesSetCaptor.get(2)).hasSize(issues.size() - 2000);
+ assertThat(newIssuesFactoryCaptor.assigneeCacheCaptor).hasSize(3);
+ assertThat(newIssuesFactoryCaptor.assigneeCacheCaptor).containsOnly(newIssuesFactoryCaptor.assigneeCacheCaptor.iterator().next());
ArgumentCaptor<Collection> collectionCaptor = forClass(Collection.class);
- verify(notificationService, times(2)).deliverEmails(collectionCaptor.capture());
- verify(notificationService, times(issues.size())).deliver(any(IssueChangeNotification.class));
- assertThat(collectionCaptor.getAllValues().get(0)).hasSize(1000);
- assertThat(collectionCaptor.getAllValues().get(1)).hasSize(issues.size() - 1000);
+ verify(notificationService, times(3)).deliverEmails(collectionCaptor.capture());
+ assertThat(collectionCaptor.getAllValues()).hasSize(3);
+ assertThat(collectionCaptor.getAllValues().get(0)).hasSize(1);
+ assertThat(collectionCaptor.getAllValues().get(1)).hasSize(1);
+ assertThat(collectionCaptor.getAllValues().get(2)).hasSize(1);
+ verify(notificationService, times(3)).deliver(any(IssuesChangesNotification.class));
+ }
+
+ /**
+ * Since the very same Set object is passed to {@link NotificationFactory#newIssuesChangesNotification(Set, Map)} and
+ * reset between each call. We must make a copy of each argument to capture what's been passed to the factory.
+ * This is of course not supported by Mockito's {@link ArgumentCaptor} and we implement this ourselves with a
+ * {@link Answer}.
+ */
+ private static class NewIssuesFactoryCaptor implements Answer<Object> {
+ private final Supplier<IssuesChangesNotification> delegate;
+ private final List<Set<DefaultIssue>> issuesSetCaptor = new ArrayList<>();
+ private final List<Map<String, UserDto>> assigneeCacheCaptor = new ArrayList<>();
+
+ private NewIssuesFactoryCaptor(Supplier<IssuesChangesNotification> delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Object answer(InvocationOnMock t) {
+ Set<DefaultIssue> issuesSet = t.getArgument(0);
+ Map<String, UserDto> assigneeCatch = t.getArgument(1);
+ issuesSetCaptor.add(ImmutableSet.copyOf(issuesSet));
+ assigneeCacheCaptor.add(ImmutableMap.copyOf(assigneeCatch));
+ return delegate.get();
+ }
}
private NewIssuesNotification createNewIssuesNotificationMock() {