diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2019-04-16 15:19:27 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2019-04-23 10:37:57 +0200 |
commit | 58bb4b37da6e32a113870b0fc98d5494379641b6 (patch) | |
tree | 027172a0a7945ea6b2f3dae5d917e1fce1a66c81 /server/sonar-ce-task-projectanalysis/src | |
parent | d9e7cb020409491b45199ab8762eb22746e3543d (diff) | |
download | sonarqube-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')
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() { |