diff options
author | Teryk Bellahsene <teryk.bellahsene@sonarsource.com> | 2015-02-27 15:32:30 +0100 |
---|---|---|
committer | Teryk Bellahsene <teryk.bellahsene@sonarsource.com> | 2015-03-04 08:43:53 +0100 |
commit | ef937b146a699cc85accbdf65973abcaf8fc8e0f (patch) | |
tree | 5befcbc929711f2060a4fcb31abbd9845a7c4703 /server | |
parent | 6f9777c016289556b7981aa894dc6a4739aa0d9d (diff) | |
download | sonarqube-ef937b146a699cc85accbdf65973abcaf8fc8e0f.tar.gz sonarqube-ef937b146a699cc85accbdf65973abcaf8fc8e0f.zip |
add information to the "New Issues" notification - SONAR-6045
Diffstat (limited to 'server')
9 files changed, 551 insertions, 105 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/ComputationContext.java b/server/sonar-server/src/main/java/org/sonar/server/computation/ComputationContext.java index 82d5e2dd4c3..9df00c9c6a5 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/ComputationContext.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/ComputationContext.java @@ -24,8 +24,6 @@ import org.sonar.api.config.Settings; import org.sonar.batch.protocol.output.BatchReport; import org.sonar.batch.protocol.output.BatchReportReader; import org.sonar.core.component.ComponentDto; -import org.sonar.core.computation.db.AnalysisReportDto; -import org.sonar.server.computation.step.ParseReportStep; import static com.google.common.base.Preconditions.checkState; @@ -33,10 +31,9 @@ public class ComputationContext { private final BatchReportReader reportReader; private final ComponentDto project; - private Settings projectSettings; - // cache of metadata as it's frequently accessed private final BatchReport.Metadata reportMetadata; + private Settings projectSettings; public ComputationContext(BatchReportReader reportReader, ComponentDto project) { this.reportReader = reportReader; diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/step/SendIssueNotificationsStep.java b/server/sonar-server/src/main/java/org/sonar/server/computation/step/SendIssueNotificationsStep.java index 1d3fc613aad..f67a92860d0 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/step/SendIssueNotificationsStep.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/step/SendIssueNotificationsStep.java @@ -22,12 +22,14 @@ package org.sonar.server.computation.step; import com.google.common.collect.ImmutableSet; import org.sonar.api.issue.internal.DefaultIssue; import org.sonar.api.resources.Qualifiers; +import org.sonar.api.utils.Durations; import org.sonar.core.component.ComponentDto; import org.sonar.server.computation.ComputationContext; import org.sonar.server.computation.issue.IssueCache; import org.sonar.server.computation.issue.RuleCache; import org.sonar.server.issue.notification.IssueChangeNotification; import org.sonar.server.issue.notification.NewIssuesNotification; +import org.sonar.server.issue.notification.NewIssuesStatistics; import org.sonar.server.notifications.NotificationService; import org.sonar.server.util.CloseableIterator; @@ -48,11 +50,13 @@ public class SendIssueNotificationsStep implements ComputationStep { private final IssueCache issueCache; private final RuleCache rules; private final NotificationService service; + private final Durations durations; - public SendIssueNotificationsStep(IssueCache issueCache, RuleCache rules, NotificationService service) { + public SendIssueNotificationsStep(IssueCache issueCache, RuleCache rules, NotificationService service, Durations durations) { this.issueCache = issueCache; this.rules = rules; this.service = service; + this.durations = durations; } @Override @@ -68,13 +72,13 @@ public class SendIssueNotificationsStep implements ComputationStep { } private void doExecute(ComputationContext context) { - NewIssuesNotification.Stats newIssueStats = new NewIssuesNotification.Stats(); + NewIssuesStatistics newIssuesStats = new NewIssuesStatistics(); CloseableIterator<DefaultIssue> issues = issueCache.traverse(); try { while (issues.hasNext()) { DefaultIssue issue = issues.next(); if (issue.isNew() && issue.resolution() == null) { - newIssueStats.add(issue); + newIssuesStats.add(issue); } else if (issue.isChanged() && issue.mustSendNotifications()) { IssueChangeNotification changeNotification = new IssueChangeNotification(); changeNotification.setRuleName(rules.ruleName(issue.ruleKey())); @@ -87,16 +91,17 @@ public class SendIssueNotificationsStep implements ComputationStep { } finally { issues.close(); } - sendNewIssuesStatistics(context, newIssueStats); + sendNewIssuesStatistics(context, newIssuesStats); } - private void sendNewIssuesStatistics(ComputationContext context, NewIssuesNotification.Stats stats) { - if (stats.size() > 0) { + private void sendNewIssuesStatistics(ComputationContext context, NewIssuesStatistics stats) { + if (stats.hasIssues()) { ComponentDto project = context.getProject(); NewIssuesNotification notification = new NewIssuesNotification(); notification.setProject(project); notification.setAnalysisDate(new Date(context.getReportMetadata().getAnalysisDate())); notification.setStatistics(project, stats); + notification.setDebt(durations.encode(stats.debt())); service.deliver(notification); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesEmailTemplate.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesEmailTemplate.java index 9caba17934a..d545ab9e9a1 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesEmailTemplate.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesEmailTemplate.java @@ -19,6 +19,7 @@ */ package org.sonar.server.issue.notification; +import com.google.common.base.Objects; import com.google.common.collect.Lists; import org.sonar.api.config.EmailSettings; import org.sonar.api.i18n.I18n; @@ -27,6 +28,9 @@ import org.sonar.api.rule.Severity; import org.sonar.api.utils.DateUtils; import org.sonar.plugins.emailnotifications.api.EmailMessage; import org.sonar.plugins.emailnotifications.api.EmailTemplate; +import org.sonar.server.issue.notification.NewIssuesStatistics.METRIC; +import org.sonar.server.user.index.UserDoc; +import org.sonar.server.user.index.UserIndex; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; @@ -42,13 +46,24 @@ public class NewIssuesEmailTemplate extends EmailTemplate { public static final String FIELD_PROJECT_NAME = "projectName"; public static final String FIELD_PROJECT_KEY = "projectKey"; public static final String FIELD_PROJECT_DATE = "projectDate"; + public static final String FIELD_PROJECT_UUID = "projectUuid"; private final EmailSettings settings; private final I18n i18n; + private final UserIndex userIndex; - public NewIssuesEmailTemplate(EmailSettings settings, I18n i18n) { + public NewIssuesEmailTemplate(EmailSettings settings, I18n i18n, UserIndex userIndex) { this.settings = settings; this.i18n = i18n; + this.userIndex = userIndex; + } + + public static String encode(String toEncode) { + try { + return URLEncoder.encode(toEncode, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("Encoding not supported", e); + } } @Override @@ -58,44 +73,105 @@ public class NewIssuesEmailTemplate extends EmailTemplate { } String projectName = notification.getFieldValue(FIELD_PROJECT_NAME); - StringBuilder sb = new StringBuilder(); - sb.append("Project: ").append(projectName).append("\n\n"); - sb.append(notification.getFieldValue("count")).append(" new issues").append("\n\n"); - sb.append(" "); + StringBuilder message = new StringBuilder(); + message.append("Project: ").append(projectName).append("\n\n"); + appendSeverity(message, notification); + appendAssignees(message, notification); + appendTags(message, notification); + appendComponents(message, notification); + appendFooter(message, notification); + + return new EmailMessage() + .setMessageId("new-issues/" + notification.getFieldValue(FIELD_PROJECT_KEY)) + .setSubject(projectName + ": " + notification.getFieldValue(METRIC.SEVERITY + ".count") + " new issues") + .setMessage(message.toString()); + } + + private void appendComponents(StringBuilder message, Notification notification) { + if (notification.getFieldValue(METRIC.COMPONENT + ".1.label") == null) { + return; + } + + message.append(" Components:\n"); + int i = 1; + while (notification.getFieldValue(METRIC.COMPONENT + "." + i + ".label") != null && i <= 5) { + String component = notification.getFieldValue(METRIC.COMPONENT + "." + i + ".label"); + message.append(" ") + .append(component) + .append(" : ") + .append(notification.getFieldValue(METRIC.COMPONENT + "." + i + ".count")) + .append("\n"); + i += 1; + } + } + + private void appendAssignees(StringBuilder message, Notification notification) { + if (notification.getFieldValue(METRIC.LOGIN + ".1.label") == null) { + return; + } + + message.append(" Assignee - "); + int i = 1; + while (notification.getFieldValue(METRIC.LOGIN + "." + i + ".label") != null && i <= 5) { + String login = notification.getFieldValue(METRIC.LOGIN + "." + i + ".label"); + UserDoc user = userIndex.getNullableByLogin(login); + String name = user == null ? null : user.name(); + message.append(Objects.firstNonNull(name, login)) + .append(": ") + .append(notification.getFieldValue(METRIC.LOGIN + "." + i + ".count")); + if (i < 5) { + message.append(" "); + } + i += 1; + } + + message.append("\n"); + } + + private void appendTags(StringBuilder message, Notification notification) { + if (notification.getFieldValue(METRIC.TAGS + ".1.label") == null) { + return; + } + + message.append(" Tags - "); + int i = 1; + while (notification.getFieldValue(METRIC.TAGS + "." + i + ".label") != null && i <= 5) { + String tag = notification.getFieldValue(METRIC.TAGS + "." + i + ".label"); + message.append(tag) + .append(": ") + .append(notification.getFieldValue(METRIC.TAGS + "." + i + ".count")); + if (i < 5) { + message.append(" "); + } + i += 1; + } + message.append("\n"); + } + + private void appendSeverity(StringBuilder message, Notification notification) { + message.append(notification.getFieldValue(METRIC.SEVERITY + ".count")).append(" new issues - Total debt: ") + .append(notification.getFieldValue(METRIC.DEBT + ".count")) + .append("\n\n") + .append(" Severity - "); for (Iterator<String> severityIterator = Lists.reverse(Severity.ALL).iterator(); severityIterator.hasNext();) { String severity = severityIterator.next(); String severityLabel = i18n.message(getLocale(), "severity." + severity, severity); - sb.append(severityLabel).append(": ").append(notification.getFieldValue("count-" + severity)); + message.append(severityLabel).append(": ").append(notification.getFieldValue(METRIC.SEVERITY + "." + severity + ".count")); if (severityIterator.hasNext()) { - sb.append(" "); + message.append(" "); } } - sb.append('\n'); - - appendFooter(sb, notification); - - return new EmailMessage() - .setMessageId("new-issues/" + notification.getFieldValue(FIELD_PROJECT_KEY)) - .setSubject(projectName + ": new issues") - .setMessage(sb.toString()); + message.append('\n'); } - private void appendFooter(StringBuilder sb, Notification notification) { - String projectUuid = notification.getFieldValue("projectUuid"); + private void appendFooter(StringBuilder message, Notification notification) { + String projectUuid = notification.getFieldValue(FIELD_PROJECT_UUID); String dateString = notification.getFieldValue(FIELD_PROJECT_DATE); if (projectUuid != null && dateString != null) { Date date = DateUtils.parseDateTime(dateString); String url = String.format("%s/issues/search#projectUuids=%s|createdAt=%s", settings.getServerBaseURL(), encode(projectUuid), encode(DateUtils.formatDateTime(date))); - sb.append("\n").append("See it in SonarQube: ").append(url).append("\n"); - } - } - - public static String encode(String toEncode) { - try { - return URLEncoder.encode(toEncode, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException("Encoding not supported", e); + message.append("\n").append("See it in SonarQube: ").append(url).append("\n"); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotification.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotification.java index ccade3f7025..a3f08c2a58b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotification.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotification.java @@ -19,16 +19,19 @@ */ package org.sonar.server.issue.notification; -import com.google.common.collect.HashMultiset; import com.google.common.collect.Multiset; import org.sonar.api.component.Component; -import org.sonar.api.issue.Issue; import org.sonar.api.notifications.Notification; import org.sonar.api.rule.Severity; import org.sonar.api.utils.DateUtils; import org.sonar.core.component.ComponentDto; +import org.sonar.server.issue.notification.NewIssuesStatistics.METRIC; import java.util.Date; +import java.util.List; + +import static org.sonar.server.issue.notification.NewIssuesEmailTemplate.*; +import static org.sonar.server.issue.notification.NewIssuesStatistics.METRIC.SEVERITY; public class NewIssuesNotification extends Notification { @@ -39,39 +42,45 @@ public class NewIssuesNotification extends Notification { } public NewIssuesNotification setAnalysisDate(Date d) { - setFieldValue("projectDate", DateUtils.formatDateTime(d)); + setFieldValue(FIELD_PROJECT_DATE, DateUtils.formatDateTime(d)); return this; } public NewIssuesNotification setProject(ComponentDto project) { - setFieldValue("projectName", project.longName()); - setFieldValue("projectKey", project.key()); - setFieldValue("projectUuid", project.uuid()); + setFieldValue(FIELD_PROJECT_NAME, project.longName()); + setFieldValue(FIELD_PROJECT_KEY, project.key()); + setFieldValue(FIELD_PROJECT_UUID, project.uuid()); return this; } - public NewIssuesNotification setStatistics(Component project, Stats stats) { - setDefaultMessage(stats.size() + " new issues on " + project.longName() + ".\n"); - setFieldValue("count", String.valueOf(stats.size())); - for (String severity : Severity.ALL) { - setFieldValue("count-" + severity, String.valueOf(stats.countIssuesWithSeverity(severity))); - } + public NewIssuesNotification setStatistics(Component project, NewIssuesStatistics stats) { + setDefaultMessage(stats.countForMetric(SEVERITY) + " new issues on " + project.longName() + ".\n"); + + setSeverityStatistics(stats); + setTop5CountsForMetric(stats, METRIC.LOGIN); + setTop5CountsForMetric(stats, METRIC.TAGS); + setTop5CountsForMetric(stats, METRIC.COMPONENT); + return this; } - public static class Stats { - private final Multiset<String> set = HashMultiset.create(); - - public void add(Issue issue) { - set.add(issue.severity()); - } + public NewIssuesNotification setDebt(String debt) { + setFieldValue(METRIC.DEBT + ".count", debt); + return this; + } - public int countIssuesWithSeverity(String severity) { - return set.count(severity); + private void setTop5CountsForMetric(NewIssuesStatistics stats, METRIC metric) { + List<Multiset.Entry<String>> loginStats = stats.statsForMetric(metric); + for (int i = 0; i < 5 && i < loginStats.size(); i++) { + setFieldValue(metric + "." + (i + 1) + ".count", String.valueOf(loginStats.get(i).getCount())); + setFieldValue(metric + "." + (i + 1) + ".label", loginStats.get(i).getElement()); } + } - public int size() { - return set.size(); + private void setSeverityStatistics(NewIssuesStatistics stats) { + setFieldValue(SEVERITY + ".count", String.valueOf(stats.countForMetric(SEVERITY))); + for (String severity : Severity.ALL) { + setFieldValue(SEVERITY + "." + severity + ".count", String.valueOf(stats.countForMetric(SEVERITY, severity))); } } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesStatistics.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesStatistics.java new file mode 100644 index 00000000000..de4c45ee43e --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesStatistics.java @@ -0,0 +1,128 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.issue.notification; + +import com.google.common.collect.HashMultiset; +import com.google.common.collect.Multiset; +import org.sonar.api.issue.Issue; +import org.sonar.api.utils.Duration; +import org.sonar.core.util.MultiSets; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.sonar.server.issue.notification.NewIssuesStatistics.METRIC.*; + +public class NewIssuesStatistics { + private Map<String, Stats> statisticsByLogin = new HashMap<>(); + private Stats globalStatistics = new Stats(); + + public void add(Issue issue) { + globalStatistics.add(issue); + String login = issue.assignee(); + if (login != null) { + statisticsForLogin(login).add(issue); + } + } + + private Stats statisticsForLogin(String login) { + if (statisticsByLogin.get(login) == null) { + statisticsByLogin.put(login, new Stats()); + } + return statisticsByLogin.get(login); + } + + public int countForMetric(METRIC metric) { + return globalStatistics.distributionFor(metric).size(); + } + + public int countForMetric(METRIC metric, String label) { + return globalStatistics.distributionFor(metric).count(label); + } + + public List<Multiset.Entry<String>> statsForMetric(METRIC metric) { + return MultiSets.listOrderedByHighestCounts(globalStatistics.distributionFor(metric)); + } + + public Duration debt() { + return globalStatistics.debt(); + } + + public boolean hasIssues() { + return globalStatistics.hasIssues(); + } + + public enum METRIC { + SEVERITY(true), TAGS(true), COMPONENT(true), LOGIN(true), DEBT(false); + private final boolean computeDistribution; + + METRIC(boolean computeDistribution) { + this.computeDistribution = computeDistribution; + } + + boolean isComputedByDistribution() { + return this.computeDistribution; + } + } + + private static class Stats { + private final Map<METRIC, Multiset<String>> distributions = new EnumMap<>(METRIC.class); + private long debtInMinutes = 0L; + + public Stats() { + for (METRIC metric : METRIC.values()) { + if (metric.isComputedByDistribution()) { + distributions.put(metric, HashMultiset.<String>create()); + } + } + } + + public void add(Issue issue) { + distributions.get(SEVERITY).add(issue.severity()); + distributions.get(COMPONENT).add(issue.componentUuid()); + if (issue.assignee() != null) { + distributions.get(LOGIN).add(issue.assignee()); + } + for (String tag : issue.tags()) { + distributions.get(TAGS).add(tag); + } + if (issue.debt() != null) { + debtInMinutes += issue.debt().toMinutes(); + } + } + + public Multiset<String> distributionFor(METRIC metric) { + checkArgument(metric.isComputedByDistribution()); + return distributions.get(metric); + } + + public Duration debt() { + return Duration.create(debtInMinutes); + } + + public boolean hasIssues() { + return distributions.get(SEVERITY) != null; + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/step/SendIssueNotificationsStepTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/step/SendIssueNotificationsStepTest.java index e0797f9ee94..a2d4eb842d0 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/step/SendIssueNotificationsStepTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/step/SendIssueNotificationsStepTest.java @@ -27,6 +27,7 @@ import org.mockito.Mockito; import org.sonar.api.issue.internal.DefaultIssue; import org.sonar.api.notifications.Notification; import org.sonar.api.rule.Severity; +import org.sonar.api.utils.Durations; import org.sonar.api.utils.System2; import org.sonar.batch.protocol.output.BatchReport; import org.sonar.server.computation.ComputationContext; @@ -50,12 +51,13 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { NotificationService notifService = mock(NotificationService.class); ComputationContext context = mock(ComputationContext.class, Mockito.RETURNS_DEEP_STUBS); IssueCache issueCache; + Durations durations = mock(Durations.class); SendIssueNotificationsStep sut; @Before public void setUp() throws Exception { issueCache = new IssueCache(temp.newFile(), System2.INSTANCE); - sut = new SendIssueNotificationsStep(issueCache, ruleCache, notifService); + sut = new SendIssueNotificationsStep(issueCache, ruleCache, notifService, durations); } @Test @@ -70,7 +72,8 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { @Test public void send_notifications_if_subscribers() throws Exception { - issueCache.newAppender().append(new DefaultIssue().setSeverity(Severity.BLOCKER)).close(); + issueCache.newAppender().append(new DefaultIssue() + .setSeverity(Severity.BLOCKER)).close(); when(context.getProject().uuid()).thenReturn("PROJECT_UUID"); when(context.getReportMetadata()).thenReturn(BatchReport.Metadata.newBuilder().build()); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest.java index c436301c8cc..b8c03d252ac 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest.java @@ -21,36 +21,59 @@ package org.sonar.server.issue.notification; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.sonar.api.config.EmailSettings; import org.sonar.api.notifications.Notification; import org.sonar.core.i18n.DefaultI18n; import org.sonar.plugins.emailnotifications.api.EmailMessage; +import org.sonar.server.user.index.UserDoc; +import org.sonar.server.user.index.UserIndex; import java.util.Locale; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.sonar.server.issue.notification.NewIssuesStatistics.METRIC.*; -@RunWith(MockitoJUnitRunner.class) public class NewIssuesEmailTemplateTest { - NewIssuesEmailTemplate template; + private static final String EMAIL_HEADER = "Project: Struts\n\n"; + private static final String EMAIL_TOTAL_ISSUES = "32 new issues - Total debt: 1d3h\n\n"; + private static final String EMAIL_ISSUES = " Severity - Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1\n"; + private static final String EMAIL_ASSIGNEES = " Assignee - robin.williams: 5 al.pacino: 7 \n"; + private static final String EMAIL_TAGS = " Tags - oscar: 3 cesar: 10 \n"; + private static final String EMAIL_COMPONENTS = " Components:\n" + + " /path/to/file : 3\n" + + " /path/to/directory : 7\n"; + private static final String EMAIL_FOOTER = "\nSee it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|createdAt=2010-05-1"; - @Mock + NewIssuesEmailTemplate template; DefaultI18n i18n; + UserIndex userIndex; @Before public void setUp() { EmailSettings settings = mock(EmailSettings.class); when(settings.getServerBaseURL()).thenReturn("http://nemo.sonarsource.org"); - template = new NewIssuesEmailTemplate(settings, i18n); + i18n = mock(DefaultI18n.class); + userIndex = mock(UserIndex.class); + // returns the login passed in parameter + when(userIndex.getNullableByLogin(anyString())).thenAnswer(new Answer<UserDoc>() { + @Override + public UserDoc answer(InvocationOnMock invocationOnMock) throws Throwable { + return new UserDoc().setName((String) invocationOnMock.getArguments()[0]); + } + }); + when(i18n.message(any(Locale.class), eq("severity.BLOCKER"), anyString())).thenReturn("Blocker"); + when(i18n.message(any(Locale.class), eq("severity.CRITICAL"), anyString())).thenReturn("Critical"); + when(i18n.message(any(Locale.class), eq("severity.MAJOR"), anyString())).thenReturn("Major"); + when(i18n.message(any(Locale.class), eq("severity.MINOR"), anyString())).thenReturn("Minor"); + when(i18n.message(any(Locale.class), eq("severity.INFO"), anyString())).thenReturn("Info"); + + template = new NewIssuesEmailTemplate(settings, i18n, userIndex); } @Test @@ -66,24 +89,44 @@ public class NewIssuesEmailTemplateTest { * From: Sonar * * Project: Foo - * 32 new issues + * 32 new issues - Total debt: 1d3h + * + * Severity - Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1 + * Assignee - robin.williams: 5 al.pacino: 7 + * Tags - oscar: 3 cesar:10 + * Components: + * /path/to/file : 3 + * /path/to/directoy : 7 * * See it in SonarQube: http://nemo.sonarsource.org/drilldown/measures/org.sonar.foo:foo?metric=new_violations * </pre> */ @Test - public void shouldFormatCommentAdded() { - Notification notification = new NewIssuesNotification() - .setFieldValue("count", "32") - .setFieldValue("count-INFO", "1") - .setFieldValue("count-MINOR", "3") - .setFieldValue("count-MAJOR", "10") - .setFieldValue("count-CRITICAL", "5") - .setFieldValue("count-BLOCKER", "0") - .setFieldValue("projectName", "Struts") - .setFieldValue("projectKey", "org.apache:struts") - .setFieldValue("projectUuid", "ABCDE") - .setFieldValue("projectDate", "2010-05-18T14:50:45+0000"); + public void format_email_with_all_fields_filled() { + Notification notification = newNotification(); + addAssignees(notification); + addTags(notification); + addComponents(notification); + + EmailMessage message = template.format(notification); + + assertThat(message.getMessageId()).isEqualTo("new-issues/org.apache:struts"); + assertThat(message.getSubject()).isEqualTo("Struts: 32 new issues"); + + // TODO datetime to be completed when test is isolated from JVM timezone + assertThat(message.getMessage()).startsWith("" + + EMAIL_HEADER + + EMAIL_TOTAL_ISSUES + + EMAIL_ISSUES + + EMAIL_ASSIGNEES + + EMAIL_TAGS + + EMAIL_COMPONENTS + + EMAIL_FOOTER); + } + + @Test + public void format_email_with_no_assignees_tags_nor_components() throws Exception { + Notification notification = newNotification(); when(i18n.message(any(Locale.class), eq("severity.BLOCKER"), anyString())).thenReturn("Blocker"); when(i18n.message(any(Locale.class), eq("severity.CRITICAL"), anyString())).thenReturn("Critical"); @@ -92,27 +135,64 @@ public class NewIssuesEmailTemplateTest { when(i18n.message(any(Locale.class), eq("severity.INFO"), anyString())).thenReturn("Info"); EmailMessage message = template.format(notification); + assertThat(message.getMessageId()).isEqualTo("new-issues/org.apache:struts"); - assertThat(message.getSubject()).isEqualTo("Struts: new issues"); + assertThat(message.getSubject()).isEqualTo("Struts: 32 new issues"); // TODO datetime to be completed when test is isolated from JVM timezone assertThat(message.getMessage()).startsWith("" + - "Project: Struts\n" + - "\n" + - "32 new issues\n" + - "\n" + - " Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1\n" + - "\n" + - "See it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|createdAt=2010-05-1"); + EMAIL_HEADER + + EMAIL_TOTAL_ISSUES + + EMAIL_ISSUES + + EMAIL_FOOTER); } @Test - public void shouldNotAddFooterIfMissingProperties() { + public void do_not_add_footer_when_properties_missing() { Notification notification = new NewIssuesNotification() - .setFieldValue("count", "32") + .setFieldValue(SEVERITY + ".count", "32") .setFieldValue("projectName", "Struts"); EmailMessage message = template.format(notification); assertThat(message.getMessage()).doesNotContain("See it"); } + + private Notification newNotification() { + return new NewIssuesNotification() + .setFieldValue("projectName", "Struts") + .setFieldValue("projectKey", "org.apache:struts") + .setFieldValue("projectUuid", "ABCDE") + .setFieldValue("projectDate", "2010-05-18T14:50:45+0000") + .setFieldValue(DEBT + ".count", "1d3h") + .setFieldValue(SEVERITY + ".count", "32") + .setFieldValue(SEVERITY + ".INFO.count", "1") + .setFieldValue(SEVERITY + ".MINOR.count", "3") + .setFieldValue(SEVERITY + ".MAJOR.count", "10") + .setFieldValue(SEVERITY + ".CRITICAL.count", "5") + .setFieldValue(SEVERITY + ".BLOCKER.count", "0"); + } + + private void addAssignees(Notification notification) { + notification + .setFieldValue(LOGIN + ".1.label", "robin.williams") + .setFieldValue(LOGIN + ".1.count", "5") + .setFieldValue(LOGIN + ".2.label", "al.pacino") + .setFieldValue(LOGIN + ".2.count", "7"); + } + + private void addTags(Notification notification) { + notification + .setFieldValue(TAGS + ".1.label", "oscar") + .setFieldValue(TAGS + ".1.count", "3") + .setFieldValue(TAGS + ".2.label", "cesar") + .setFieldValue(TAGS + ".2.count", "10"); + } + + private void addComponents(Notification notification) { + notification + .setFieldValue(COMPONENT + ".1.label", "/path/to/file") + .setFieldValue(COMPONENT + ".1.count", "3") + .setFieldValue(COMPONENT + ".2.label", "/path/to/directory") + .setFieldValue(COMPONENT + ".2.count", "7"); + } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationTest.java index 4c208547406..73c52db97c8 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationTest.java @@ -17,27 +17,105 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + package org.sonar.server.issue.notification; +import com.google.common.collect.Lists; import org.junit.Test; import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.api.rule.Severity; +import org.sonar.api.utils.DateUtils; +import org.sonar.api.utils.Duration; +import org.sonar.core.component.ComponentDto; +import org.sonar.server.component.ComponentTesting; + +import java.util.Date; import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.server.issue.notification.NewIssuesStatistics.METRIC.*; public class NewIssuesNotificationTest { + NewIssuesNotification sut = new NewIssuesNotification(); + NewIssuesStatistics stats = new NewIssuesStatistics(); + + @Test + public void set_project() throws Exception { + ComponentDto component = ComponentTesting.newProjectDto() + .setLongName("project-long-name") + .setUuid("project-uuid") + .setKey("project-key"); + + sut.setProject(component); + + assertThat(sut.getFieldValue(NewIssuesEmailTemplate.FIELD_PROJECT_NAME)).isEqualTo("project-long-name"); + assertThat(sut.getFieldValue(NewIssuesEmailTemplate.FIELD_PROJECT_UUID)).isEqualTo("project-uuid"); + assertThat(sut.getFieldValue(NewIssuesEmailTemplate.FIELD_PROJECT_KEY)).isEqualTo("project-key"); + } + + @Test + public void set_date() throws Exception { + Date date = new Date(); + + sut.setAnalysisDate(date); + + assertThat(sut.getFieldValue(NewIssuesEmailTemplate.FIELD_PROJECT_DATE)).isEqualTo(DateUtils.formatDateTime(date)); + } + @Test - public void stats() { - NewIssuesNotification.Stats sut = new NewIssuesNotification.Stats(); - assertThat(sut.size()).isEqualTo(0); - - sut.add(new DefaultIssue().setSeverity("MINOR")); - sut.add(new DefaultIssue().setSeverity("BLOCKER")); - sut.add(new DefaultIssue().setSeverity("MINOR")); - - assertThat(sut.size()).isEqualTo(3); - assertThat(sut.countIssuesWithSeverity("INFO")).isEqualTo(0); - assertThat(sut.countIssuesWithSeverity("MINOR")).isEqualTo(2); - assertThat(sut.countIssuesWithSeverity("BLOCKER")).isEqualTo(1); + public void set_statistics() throws Exception { + ComponentDto component = ComponentTesting.newProjectDto() + .setLongName("project-long-name"); + addIssueNTimes(newIssue1(), 5); + addIssueNTimes(newIssue2(), 3); + + sut.setStatistics(component, stats); + + assertThat(sut.getFieldValue(SEVERITY + ".INFO.count")).isEqualTo("5"); + assertThat(sut.getFieldValue(SEVERITY + ".BLOCKER.count")).isEqualTo("3"); + assertThat(sut.getFieldValue(LOGIN + ".1.label")).isEqualTo("maynard"); + assertThat(sut.getFieldValue(LOGIN + ".1.count")).isEqualTo("5"); + assertThat(sut.getFieldValue(LOGIN + ".2.label")).isEqualTo("keenan"); + assertThat(sut.getFieldValue(LOGIN + ".2.count")).isEqualTo("3"); + assertThat(sut.getFieldValue(TAGS + ".1.label")).isEqualTo("owasp"); + assertThat(sut.getFieldValue(TAGS + ".1.count")).isEqualTo("8"); + assertThat(sut.getFieldValue(TAGS + ".2.label")).isEqualTo("bug"); + assertThat(sut.getFieldValue(TAGS + ".2.count")).isEqualTo("5"); + assertThat(sut.getFieldValue(COMPONENT + ".1.label")).isEqualTo("file-uuid"); + assertThat(sut.getFieldValue(COMPONENT + ".1.count")).isEqualTo("5"); + assertThat(sut.getFieldValue(COMPONENT + ".2.label")).isEqualTo("directory-uuid"); + assertThat(sut.getFieldValue(COMPONENT + ".2.count")).isEqualTo("3"); + assertThat(sut.getDefaultMessage()).startsWith("8 new issues on project-long-name"); + } + + @Test + public void set_debt() throws Exception { + sut.setDebt("55min"); + + assertThat(sut.getFieldValue(DEBT + ".count")).isEqualTo("55min"); + } + + private void addIssueNTimes(DefaultIssue issue, int times) { + for (int i = 0; i < times; i++) { + stats.add(issue); + } + } + + private DefaultIssue newIssue1() { + return new DefaultIssue() + .setAssignee("maynard") + .setComponentUuid("file-uuid") + .setSeverity(Severity.INFO) + .setTags(Lists.newArrayList("bug", "owasp")) + .setDebt(Duration.create(5L)); + } + + private DefaultIssue newIssue2() { + return new DefaultIssue() + .setAssignee("keenan") + .setComponentUuid("directory-uuid") + .setSeverity(Severity.BLOCKER) + .setTags(Lists.newArrayList("owasp")) + .setDebt(Duration.create(10L)); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesStatisticsTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesStatisticsTest.java new file mode 100644 index 00000000000..2bc1c2c3d66 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesStatisticsTest.java @@ -0,0 +1,70 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.issue.notification; + +import com.google.common.collect.Lists; +import org.junit.Test; +import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.api.rule.Severity; +import org.sonar.api.utils.Duration; +import org.sonar.server.issue.notification.NewIssuesStatistics.METRIC; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NewIssuesStatisticsTest { + + NewIssuesStatistics sut = new NewIssuesStatistics(); + + @Test + public void add_issues_with_correct_global_statistics() throws Exception { + DefaultIssue issue = defaultIssue(); + + sut.add(issue); + sut.add(issue.setAssignee("james")); + sut.add(issue.setAssignee("keenan")); + + assertThat(countDistribution(METRIC.LOGIN, "maynard")).isEqualTo(1); + assertThat(countDistribution(METRIC.LOGIN, "james")).isEqualTo(1); + assertThat(countDistribution(METRIC.LOGIN, "keenan")).isEqualTo(1); + assertThat(countDistribution(METRIC.LOGIN, "wrong.login")).isEqualTo(0); + assertThat(countDistribution(METRIC.COMPONENT, "file-uuid")).isEqualTo(3); + assertThat(countDistribution(METRIC.COMPONENT, "wrong-uuid")).isEqualTo(0); + assertThat(countDistribution(METRIC.SEVERITY, Severity.INFO)).isEqualTo(3); + assertThat(countDistribution(METRIC.SEVERITY, Severity.CRITICAL)).isEqualTo(0); + assertThat(countDistribution(METRIC.TAGS, "owasp")).isEqualTo(3); + assertThat(countDistribution(METRIC.TAGS, "wrong-tag")).isEqualTo(0); + assertThat(sut.debt().toMinutes()).isEqualTo(15L); + } + + private int countDistribution(METRIC metric, String label) { + return sut.countForMetric(metric, label); + } + + private DefaultIssue defaultIssue() { + return new DefaultIssue() + .setAssignee("maynard") + .setComponentUuid("file-uuid") + .setNew(true) + .setSeverity(Severity.INFO) + .setTags(Lists.newArrayList("bug", "owasp")) + .setDebt(Duration.create(5L)); + } +} |