diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2017-09-12 12:37:15 +0200 |
---|---|---|
committer | Eric Hartmann <hartmann.eric@gmail.Com> | 2017-10-02 13:03:35 +0200 |
commit | c9f01d74767b55cad280b1056ea71a3d9650c908 (patch) | |
tree | db5a7c9faa69f950239a4043139ad87e81b50813 /server | |
parent | 803439a16e9b40da4ad9a5baa9cd84a407c607b5 (diff) | |
download | sonarqube-c9f01d74767b55cad280b1056ea71a3d9650c908.tar.gz sonarqube-c9f01d74767b55cad280b1056ea71a3d9650c908.zip |
SONAR-9144 support leak period when computing issue statistics
during report processing
Diffstat (limited to 'server')
12 files changed, 655 insertions, 113 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/SendIssueNotificationsStep.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/SendIssueNotificationsStep.java index 6e26dcbad07..cd1dde632dd 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/SendIssueNotificationsStep.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/SendIssueNotificationsStep.java @@ -21,10 +21,14 @@ package org.sonar.server.computation.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.Date; import java.util.Map; import java.util.Optional; import java.util.Set; +import org.sonar.api.issue.Issue; +import org.sonar.api.utils.Duration; import org.sonar.core.issue.DefaultIssue; import org.sonar.core.util.CloseableIterator; import org.sonar.server.computation.task.projectanalysis.analysis.AnalysisMetadataHolder; @@ -85,7 +89,8 @@ public class SendIssueNotificationsStep implements ComputationStep { } private void doExecute(Component project) { - NewIssuesStatistics newIssuesStats = new NewIssuesStatistics(); + long analysisDate = analysisMetadataHolder.getAnalysisDate(); + NewIssuesStatistics newIssuesStats = new NewIssuesStatistics(i -> i.isNew() && i.creationDate().getTime() >= truncateToSeconds(analysisDate)); CloseableIterator<DefaultIssue> issues = issueCache.traverse(); try { processIssues(newIssuesStats, issues, project); @@ -93,12 +98,21 @@ public class SendIssueNotificationsStep implements ComputationStep { issues.close(); } if (newIssuesStats.hasIssues()) { - long analysisDate = analysisMetadataHolder.getAnalysisDate(); sendNewIssuesNotification(newIssuesStats, project, analysisDate); sendNewIssuesNotificationToAssignees(newIssuesStats, project, analysisDate); } } + /** + * Truncated the analysis date to seconds before comparing it to {@link Issue#creationDate()} is required because + * {@link DefaultIssue#setCreationDate(Date)} does it. + */ + private static long truncateToSeconds(long analysisDate) { + Instant instant = new Date(analysisDate).toInstant(); + instant = instant.truncatedTo(ChronoUnit.SECONDS); + return Date.from(instant).getTime(); + } + private void processIssues(NewIssuesStatistics newIssuesStats, CloseableIterator<DefaultIssue> issues, Component project) { while (issues.hasNext()) { DefaultIssue issue = issues.next(); @@ -126,7 +140,7 @@ public class SendIssueNotificationsStep implements ComputationStep { .setProject(project.getPublicKey(), project.getUuid(), project.getName(), getBranchName()) .setAnalysisDate(new Date(analysisDate)) .setStatistics(project.getName(), globalStatistics) - .setDebt(globalStatistics.debt()); + .setDebt(Duration.create(globalStatistics.effort().getTotal())); service.deliver(notification); } @@ -142,7 +156,7 @@ public class SendIssueNotificationsStep implements ComputationStep { .setProject(project.getPublicKey(), project.getUuid(), project.getName(), getBranchName()) .setAnalysisDate(new Date(analysisDate)) .setStatistics(project.getName(), assigneeStatistics) - .setDebt(assigneeStatistics.debt()); + .setDebt(Duration.create(assigneeStatistics.effort().getTotal())); service.deliver(myNewIssuesNotification); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java index caecaa2851e..8b66c747288 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java @@ -98,7 +98,7 @@ public abstract class AbstractNewIssuesEmailTemplate extends EmailTemplate { return String.format("%s: %s new issues (new debt: %s)", projectName, notification.getFieldValue(Metric.SEVERITY + COUNT), - notification.getFieldValue(Metric.DEBT + COUNT)); + notification.getFieldValue(Metric.EFFORT + COUNT)); } private static boolean doNotHaveValue(Notification notification, Metric metric) { @@ -149,7 +149,7 @@ public abstract class AbstractNewIssuesEmailTemplate extends EmailTemplate { message .append(String.format("%s new issues (new debt: %s)", notification.getFieldValue(Metric.SEVERITY + COUNT), - notification.getFieldValue(Metric.DEBT + COUNT))) + notification.getFieldValue(Metric.EFFORT + COUNT))) .append(NEW_LINE).append(NEW_LINE) .append(TAB) .append("Severity") diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/DistributedMetricStatsInt.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/DistributedMetricStatsInt.java new file mode 100644 index 00000000000..b0a331270f4 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/DistributedMetricStatsInt.java @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.issue.notification; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class DistributedMetricStatsInt { + private MetricStatsInt globalStats = new MetricStatsInt(); + private Map<String, MetricStatsInt> statsPerLabel = new HashMap<>(); + + DistributedMetricStatsInt increment(String label, boolean onLeak) { + this.globalStats.increment(onLeak); + statsPerLabel.computeIfAbsent(label, l -> new MetricStatsInt()).increment(onLeak); + return this; + } + + Map<String, MetricStatsInt> getForLabels() { + return Collections.unmodifiableMap(statsPerLabel); + } + + public Optional<MetricStatsInt> getForLabel(String label) { + return Optional.ofNullable(statsPerLabel.get(label)); + } + + public int getOnLeak() { + return globalStats.getOnLeak(); + } + + public int getOffLeak() { + return globalStats.getOffLeak(); + } + + public int getTotal() { + return globalStats.getTotal(); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/MetricStatsInt.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/MetricStatsInt.java new file mode 100644 index 00000000000..f1d546c5293 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/MetricStatsInt.java @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.issue.notification; + +public class MetricStatsInt { + private int onLeak = 0; + private int offLeak = 0; + + MetricStatsInt increment(boolean onLeak) { + if (onLeak) { + this.onLeak += 1; + } else { + this.offLeak += 1; + } + return this; + } + + public int getOnLeak() { + return onLeak; + } + + public int getOffLeak() { + return offLeak; + } + + public int getTotal() { + return onLeak + offLeak; + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/notification/MetricStatsLong.java b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/MetricStatsLong.java new file mode 100644 index 00000000000..ae43cf4ee2a --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/notification/MetricStatsLong.java @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.issue.notification; + +public class MetricStatsLong { + private long onLeak = 0; + private long offLeak = 0; + + MetricStatsLong add(long toAdd, boolean onLeak) { + if (onLeak) { + this.onLeak += toAdd; + } else { + this.offLeak += toAdd; + } + return this; + } + + public long getOnLeak() { + return onLeak; + } + + public long getOffLeak() { + return offLeak; + } + + public long getTotal() { + return onLeak + offLeak; + } + +} 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 69a91c20081..6d2df27822e 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,9 +19,11 @@ */ package org.sonar.server.issue.notification; -import com.google.common.collect.Multiset; +import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.Map; +import java.util.function.ToIntFunction; import javax.annotation.Nullable; import org.sonar.api.notifications.Notification; import org.sonar.api.rule.RuleKey; @@ -29,6 +31,7 @@ import org.sonar.api.rule.Severity; import org.sonar.api.utils.DateUtils; import org.sonar.api.utils.Duration; import org.sonar.api.utils.Durations; +import org.sonar.core.util.stream.MoreCollectors; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.rule.RuleDefinitionDto; @@ -82,7 +85,7 @@ public class NewIssuesNotification extends Notification { } public NewIssuesNotification setStatistics(String projectName, NewIssuesStatistics.Stats stats) { - setDefaultMessage(stats.countForMetric(SEVERITY) + " new issues on " + projectName + ".\n"); + setDefaultMessage(stats.getDistributedMetricStats(SEVERITY).getTotal() + " new issues on " + projectName + ".\n"); try (DbSession dbSession = dbClient.openSession(false)) { setSeverityStatistics(stats); @@ -95,59 +98,77 @@ public class NewIssuesNotification extends Notification { return this; } - protected void setRuleStatistics(DbSession dbSession, NewIssuesStatistics.Stats stats) { + private void setRuleStatistics(DbSession dbSession, NewIssuesStatistics.Stats stats) { Metric metric = Metric.RULE; - List<Multiset.Entry<String>> metricStats = stats.statsForMetric(metric); - for (int i = 0; i < 5 && i < metricStats.size(); i++) { - String ruleKey = metricStats.get(i).getElement(); + int i = 1; + for (Map.Entry<String, MetricStatsInt> ruleStats : fiveBiggest(stats.getDistributedMetricStats(metric), MetricStatsInt::getTotal)) { + String ruleKey = ruleStats.getKey(); RuleDefinitionDto rule = dbClient.ruleDao().selectOrFailDefinitionByKey(dbSession, RuleKey.parse(ruleKey)); String name = rule.getName() + " (" + rule.getLanguage() + ")"; - setFieldValue(metric + DOT + (i + 1) + LABEL, name); - setFieldValue(metric + DOT + (i + 1) + COUNT, String.valueOf(metricStats.get(i).getCount())); + setFieldValue(metric + DOT + i + LABEL, name); + setFieldValue(metric + DOT + i + COUNT, String.valueOf(ruleStats.getValue().getTotal())); + i++; } } - protected void setComponentsStatistics(DbSession dbSession, NewIssuesStatistics.Stats stats) { + private void setComponentsStatistics(DbSession dbSession, NewIssuesStatistics.Stats stats) { Metric metric = Metric.COMPONENT; - List<Multiset.Entry<String>> componentStats = stats.statsForMetric(metric); - for (int i = 0; i < 5 && i < componentStats.size(); i++) { - String uuid = componentStats.get(i).getElement(); + int i = 1; + for (Map.Entry<String, MetricStatsInt> componentStats : fiveBiggest(stats.getDistributedMetricStats(metric), MetricStatsInt::getTotal)) { + String uuid = componentStats.getKey(); String componentName = dbClient.componentDao().selectOrFailByUuid(dbSession, uuid).name(); - setFieldValue(metric + DOT + (i + 1) + LABEL, componentName); - setFieldValue(metric + DOT + (i + 1) + COUNT, String.valueOf(componentStats.get(i).getCount())); + setFieldValue(metric + DOT + i + LABEL, componentName); + setFieldValue(metric + DOT + i + COUNT, String.valueOf(componentStats.getValue().getTotal())); + i++; } } - protected void setTagsStatistics(NewIssuesStatistics.Stats stats) { + private void setTagsStatistics(NewIssuesStatistics.Stats stats) { Metric metric = Metric.TAG; - List<Multiset.Entry<String>> metricStats = stats.statsForMetric(metric); - for (int i = 0; i < 5 && i < metricStats.size(); i++) { - setFieldValue(metric + DOT + (i + 1) + COUNT, String.valueOf(metricStats.get(i).getCount())); - setFieldValue(metric + DOT + (i + 1) + ".label", metricStats.get(i).getElement()); + int i = 1; + for (Map.Entry<String, MetricStatsInt> tagStats : fiveBiggest(stats.getDistributedMetricStats(metric), MetricStatsInt::getTotal)) { + setFieldValue(metric + DOT + i + COUNT, String.valueOf(tagStats.getValue().getTotal())); + setFieldValue(metric + DOT + i + ".label", tagStats.getKey()); + i++; } } - protected void setAssigneesStatistics(NewIssuesStatistics.Stats stats) { + private void setAssigneesStatistics(NewIssuesStatistics.Stats stats) { Metric metric = Metric.ASSIGNEE; - List<Multiset.Entry<String>> metricStats = stats.statsForMetric(metric); - for (int i = 0; i < 5 && i < metricStats.size(); i++) { - String login = metricStats.get(i).getElement(); + ToIntFunction<MetricStatsInt> biggerCriteria = MetricStatsInt::getTotal; + int i = 1; + for (Map.Entry<String, MetricStatsInt> assigneeStats : fiveBiggest(stats.getDistributedMetricStats(metric), biggerCriteria)) { + String login = assigneeStats.getKey(); UserDoc user = userIndex.getNullableByLogin(login); String name = user == null ? login : user.name(); - setFieldValue(metric + DOT + (i + 1) + LABEL, name); - setFieldValue(metric + DOT + (i + 1) + COUNT, String.valueOf(metricStats.get(i).getCount())); + setFieldValue(metric + DOT + i + LABEL, name); + setFieldValue(metric + DOT + i + COUNT, String.valueOf(biggerCriteria.applyAsInt(assigneeStats.getValue()))); + i++; } } + private static List<Map.Entry<String, MetricStatsInt>> fiveBiggest(DistributedMetricStatsInt distributedMetricStatsInt, ToIntFunction<MetricStatsInt> biggerCriteria) { + Comparator<Map.Entry<String, MetricStatsInt>> comparator = Comparator.comparingInt(a -> biggerCriteria.applyAsInt(a.getValue())); + return distributedMetricStatsInt.getForLabels() + .entrySet() + .stream() + .sorted(comparator.reversed()) + .limit(5) + .collect(MoreCollectors.toList(5)); + } + public NewIssuesNotification setDebt(Duration debt) { - setFieldValue(Metric.DEBT + COUNT, durations.format(debt)); + setFieldValue(Metric.EFFORT + COUNT, durations.format(debt)); return this; } - protected void setSeverityStatistics(NewIssuesStatistics.Stats stats) { - setFieldValue(SEVERITY + COUNT, String.valueOf(stats.countForMetric(SEVERITY))); + private void setSeverityStatistics(NewIssuesStatistics.Stats stats) { + DistributedMetricStatsInt distributedMetricStats = stats.getDistributedMetricStats(SEVERITY); + setFieldValue(SEVERITY + COUNT, String.valueOf(distributedMetricStats.getTotal())); for (String severity : Severity.ALL) { - setFieldValue(SEVERITY + DOT + severity + COUNT, String.valueOf(stats.countForMetric(SEVERITY, severity))); + setFieldValue( + SEVERITY + DOT + severity + COUNT, + String.valueOf(distributedMetricStats.getForLabel(severity).map(MetricStatsInt::getTotal).orElse(0))); } } 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 index b4746bed5f5..8410d8a9caa 100644 --- 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 @@ -19,18 +19,14 @@ */ package org.sonar.server.issue.notification; -import com.google.common.collect.HashMultiset; -import com.google.common.collect.Multiset; -import com.google.common.collect.Multisets; import java.util.EnumMap; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; +import java.util.function.Predicate; import org.sonar.api.issue.Issue; import org.sonar.api.rule.RuleKey; import org.sonar.api.utils.Duration; -import static com.google.common.base.Preconditions.checkArgument; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.ASSIGNEE; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.COMPONENT; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.RULE; @@ -38,21 +34,23 @@ import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.SEV import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.TAG; public class NewIssuesStatistics { - private Map<String, Stats> assigneesStatistics = new LinkedHashMap<>(); - private Stats globalStatistics = new Stats(); + private final Predicate<Issue> onLeakPredicate; + private final Map<String, Stats> assigneesStatistics = new LinkedHashMap<>(); + private final Stats globalStatistics; + + public NewIssuesStatistics(Predicate<Issue> onLeakPredicate) { + this.onLeakPredicate = onLeakPredicate; + this.globalStatistics = new Stats(onLeakPredicate); + } public void add(Issue issue) { globalStatistics.add(issue); String login = issue.assignee(); if (login != null) { - getOrCreate(login).add(issue); + assigneesStatistics.computeIfAbsent(login, a -> new Stats(onLeakPredicate)).add(issue); } } - private Stats getOrCreate(String assignee) { - return assigneesStatistics.computeIfAbsent(assignee, a -> new Stats()); - } - public Map<String, Stats> assigneesStatistics() { return assigneesStatistics; } @@ -66,7 +64,7 @@ public class NewIssuesStatistics { } enum Metric { - SEVERITY(true), TAG(true), COMPONENT(true), ASSIGNEE(true), DEBT(false), RULE(true); + SEVERITY(true), TAG(true), COMPONENT(true), ASSIGNEE(true), EFFORT(false), RULE(true); private final boolean isComputedByDistribution; Metric(boolean isComputedByDistribution) { @@ -79,59 +77,52 @@ public class NewIssuesStatistics { } public static class Stats { - private final Map<Metric, Multiset<String>> distributions = new EnumMap<>(Metric.class); - private long debtInMinutes = 0L; + private final Predicate<Issue> onLeakPredicate; + private final Map<Metric, DistributedMetricStatsInt> distributions = new EnumMap<>(Metric.class); + private MetricStatsLong effortStats = new MetricStatsLong(); - public Stats() { + public Stats(Predicate<Issue> onLeakPredicate) { + this.onLeakPredicate = onLeakPredicate; for (Metric metric : Metric.values()) { if (metric.isComputedByDistribution()) { - distributions.put(metric, HashMultiset.<String>create()); + distributions.put(metric, new DistributedMetricStatsInt()); } } } public void add(Issue issue) { - distributions.get(SEVERITY).add(issue.severity()); - distributions.get(COMPONENT).add(issue.componentUuid()); + boolean isOnLeak = onLeakPredicate.test(issue); + distributions.get(SEVERITY).increment(issue.severity(), isOnLeak); + distributions.get(COMPONENT).increment(issue.componentUuid(), isOnLeak); RuleKey ruleKey = issue.ruleKey(); if (ruleKey != null) { - distributions.get(RULE).add(ruleKey.toString()); + distributions.get(RULE).increment(ruleKey.toString(), isOnLeak); } - if (issue.assignee() != null) { - distributions.get(ASSIGNEE).add(issue.assignee()); + String assignee = issue.assignee(); + if (assignee != null) { + distributions.get(ASSIGNEE).increment(assignee, isOnLeak); } for (String tag : issue.tags()) { - distributions.get(TAG).add(tag); + distributions.get(TAG).increment(tag, isOnLeak); } - Duration debt = issue.debt(); - if (debt != null) { - debtInMinutes += debt.toMinutes(); + Duration effort = issue.effort(); + if (effort != null) { + effortStats.add(effort.toMinutes(), isOnLeak); } } - public int countForMetric(Metric metric) { - return distributionFor(metric).size(); - } - - public int countForMetric(Metric metric, String label) { - return distributionFor(metric).count(label); + public DistributedMetricStatsInt getDistributedMetricStats(Metric metric) { + return distributions.get(metric); } - public Duration debt() { - return Duration.create(debtInMinutes); + public MetricStatsLong effort() { + return effortStats; } public boolean hasIssues() { - return !distributionFor(SEVERITY).isEmpty(); - } - - public List<Multiset.Entry<String>> statsForMetric(Metric metric) { - return Multisets.copyHighestCountFirst(distributionFor(metric)).entrySet().asList(); + return getDistributedMetricStats(SEVERITY).getTotal() > 0; } - private Multiset<String> distributionFor(Metric metric) { - checkArgument(metric.isComputedByDistribution()); - return distributions.get(metric); - } } + } diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/SendIssueNotificationsStepTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/SendIssueNotificationsStepTest.java index 824c72a9129..384f05314cb 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/SendIssueNotificationsStepTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/SendIssueNotificationsStepTest.java @@ -123,7 +123,9 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { @Test public void send_global_new_issues_notification() throws Exception { issueCache.newAppender().append( - new DefaultIssue().setSeverity(Severity.BLOCKER).setEffort(ISSUE_DURATION)).close(); + new DefaultIssue().setSeverity(Severity.BLOCKER).setEffort(ISSUE_DURATION) + .setCreationDate(new Date(ANALYSE_DATE))) + .close(); when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), SendIssueNotificationsStep.NOTIF_TYPES)).thenReturn(true); @@ -142,8 +144,7 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { ComponentDto branch = newProjectBranch(project, newBranchDto(project).setKey(BRANCH_NAME)); ComponentDto file = newFileDto(branch); treeRootHolder.setRoot(builder(Type.PROJECT, 2).setKey(branch.getDbKey()).setPublicKey(branch.getKey()).setName(branch.longName()).setUuid(branch.uuid()).addChildren( - builder(Component.Type.FILE, 11).setKey(file.getDbKey()).setPublicKey(file.getKey()).setName(file.longName()).build() - ).build()); + builder(Component.Type.FILE, 11).setKey(file.getDbKey()).setPublicKey(file.getKey()).setName(file.longName()).build()).build()); issueCache.newAppender().append( new DefaultIssue().setSeverity(Severity.BLOCKER).setEffort(ISSUE_DURATION)).close(); @@ -162,7 +163,9 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { @Test public void send_new_issues_notification_to_user() throws Exception { issueCache.newAppender().append( - new DefaultIssue().setSeverity(Severity.BLOCKER).setEffort(ISSUE_DURATION).setAssignee(ISSUE_ASSIGNEE)).close(); + new DefaultIssue().setSeverity(Severity.BLOCKER).setEffort(ISSUE_DURATION).setAssignee(ISSUE_ASSIGNEE) + .setCreationDate(new Date(ANALYSE_DATE))) + .close(); when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), SendIssueNotificationsStep.NOTIF_TYPES)).thenReturn(true); @@ -182,9 +185,7 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { ComponentDto file = newFileDto(project).setDbKey(FILE.getKey()).setLongName(FILE.getName()); RuleDefinitionDto ruleDefinitionDto = newRule(); DefaultIssue issue = newIssue(ruleDefinitionDto, project, file).toDefaultIssue() - .setNew(false) - .setChanged(true) - .setSendNotifications(true); + .setNew(false).setChanged(true).setSendNotifications(true).setCreationDate(new Date(ANALYSE_DATE)); ruleRepository.add(ruleDefinitionDto.getKey()).setName(ruleDefinitionDto.getName()); issueCache.newAppender().append(issue).close(); when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), SendIssueNotificationsStep.NOTIF_TYPES)).thenReturn(true); @@ -209,8 +210,7 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { ComponentDto branch = newProjectBranch(project, newBranchDto(project).setKey(BRANCH_NAME)); ComponentDto file = newFileDto(branch); treeRootHolder.setRoot(builder(Type.PROJECT, 2).setKey(branch.getDbKey()).setPublicKey(branch.getKey()).setName(branch.longName()).setUuid(branch.uuid()).addChildren( - builder(Component.Type.FILE, 11).setKey(file.getDbKey()).setPublicKey(file.getKey()).setName(file.longName()).build() - ).build()); + builder(Component.Type.FILE, 11).setKey(file.getDbKey()).setPublicKey(file.getKey()).setName(file.longName()).build()).build()); RuleDefinitionDto ruleDefinitionDto = newRule(); DefaultIssue issue = newIssue(ruleDefinitionDto, branch, file).toDefaultIssue() .setNew(false) diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest.java index 8922e3fe0c6..5bd31513272 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest.java @@ -20,7 +20,6 @@ package org.sonar.server.issue.notification; import java.io.IOException; -import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.Locale; @@ -43,7 +42,7 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.COMPONENT; -import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.DEBT; +import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.EFFORT; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.RULE; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.SEVERITY; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.TAG; @@ -154,7 +153,7 @@ public class MyNewIssuesEmailTemplateTest { .setFieldValue("projectUuid", "ABCDE") .setFieldValue("projectDate", "2010-05-18T14:50:45+0000") .setFieldValue("assignee", "lo.gin") - .setFieldValue(DEBT + ".count", "1d3h") + .setFieldValue(EFFORT + ".count", "1d3h") .setFieldValue(SEVERITY + ".count", "32") .setFieldValue(SEVERITY + ".INFO.count", "1") .setFieldValue(SEVERITY + ".MINOR.count", "3") 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 b2116b238fd..7c8fce8dcdd 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,7 +21,6 @@ package org.sonar.server.issue.notification; import java.io.IOException; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; import org.junit.Before; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; @@ -44,7 +43,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.ASSIGNEE; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.COMPONENT; -import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.DEBT; +import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.EFFORT; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.RULE; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.SEVERITY; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.TAG; @@ -154,7 +153,7 @@ public class NewIssuesEmailTemplateTest { .setFieldValue("projectKey", "org.apache:struts") .setFieldValue("projectUuid", "ABCDE") .setFieldValue("projectDate", "2010-05-18T14:50:45+0000") - .setFieldValue(DEBT + ".count", "1d3h") + .setFieldValue(EFFORT + ".count", "1d3h") .setFieldValue(SEVERITY + ".count", "32") .setFieldValue(SEVERITY + ".INFO.count", "1") .setFieldValue(SEVERITY + ".MINOR.count", "3") 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 1c62a3c083c..f5ff4dd9a43 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 @@ -41,14 +41,14 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.ASSIGNEE; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.COMPONENT; -import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.DEBT; +import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.EFFORT; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.RULE; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.SEVERITY; import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.TAG; public class NewIssuesNotificationTest { - NewIssuesStatistics.Stats stats = new NewIssuesStatistics.Stats(); + NewIssuesStatistics.Stats stats = new NewIssuesStatistics.Stats(i -> true); UserIndex userIndex = mock(UserIndex.class); DbClient dbClient = mock(DbClient.class, Mockito.RETURNS_DEEP_STUBS); Durations durations = mock(Durations.class); @@ -121,7 +121,7 @@ public class NewIssuesNotificationTest { underTest.setDebt(Duration.create(55)); - assertThat(underTest.getFieldValue(DEBT + ".count")).isEqualTo("55 min"); + assertThat(underTest.getFieldValue(EFFORT + ".count")).isEqualTo("55 min"); } private void addIssueNTimes(DefaultIssue issue, int times) { 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 index 8642b392be8..6a56f8de7d0 100644 --- 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 @@ -20,52 +20,419 @@ package org.sonar.server.issue.notification; import com.google.common.collect.Lists; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import javax.annotation.CheckForNull; import org.junit.Test; +import org.sonar.api.issue.Issue; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; import org.sonar.api.utils.Duration; import org.sonar.core.issue.DefaultIssue; import org.sonar.server.issue.notification.NewIssuesStatistics.Metric; +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; import static org.assertj.core.api.Assertions.assertThat; public class NewIssuesStatisticsTest { - NewIssuesStatistics underTest = new NewIssuesStatistics(); + NewIssuesStatistics underTest = new NewIssuesStatistics(Issue::isNew); @Test public void add_issues_with_correct_global_statistics() { - DefaultIssue issue = defaultIssue(); + DefaultIssue issue = new DefaultIssue() + .setAssignee("maynard") + .setComponentUuid("file-uuid") + .setNew(true) + .setSeverity(Severity.INFO) + .setRuleKey(RuleKey.of("SonarQube", "rule-the-world")) + .setTags(Lists.newArrayList("bug", "owasp")) + .setEffort(Duration.create(5L)); underTest.add(issue); underTest.add(issue.setAssignee("james")); underTest.add(issue.setAssignee("keenan")); - assertThat(countDistribution(Metric.ASSIGNEE, "maynard")).isEqualTo(1); - assertThat(countDistribution(Metric.ASSIGNEE, "james")).isEqualTo(1); - assertThat(countDistribution(Metric.ASSIGNEE, "keenan")).isEqualTo(1); - assertThat(countDistribution(Metric.ASSIGNEE, "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.TAG, "owasp")).isEqualTo(3); - assertThat(countDistribution(Metric.TAG, "wrong-tag")).isEqualTo(0); - assertThat(countDistribution(Metric.RULE, "SonarQube:rule-the-world")).isEqualTo(3); - assertThat(countDistribution(Metric.RULE, "SonarQube:has-a-fake-rule")).isEqualTo(0); - assertThat(underTest.globalStatistics().debt().toMinutes()).isEqualTo(15L); + assertThat(countDistributionTotal(Metric.ASSIGNEE, "maynard")).isEqualTo(1); + assertThat(countDistributionTotal(Metric.ASSIGNEE, "james")).isEqualTo(1); + assertThat(countDistributionTotal(Metric.ASSIGNEE, "keenan")).isEqualTo(1); + assertThat(countDistributionTotal(Metric.ASSIGNEE, "wrong.login")).isNull(); + assertThat(countDistributionTotal(Metric.COMPONENT, "file-uuid")).isEqualTo(3); + assertThat(countDistributionTotal(Metric.COMPONENT, "wrong-uuid")).isNull(); + assertThat(countDistributionTotal(Metric.SEVERITY, Severity.INFO)).isEqualTo(3); + assertThat(countDistributionTotal(Metric.SEVERITY, Severity.CRITICAL)).isNull(); + assertThat(countDistributionTotal(Metric.TAG, "owasp")).isEqualTo(3); + assertThat(countDistributionTotal(Metric.TAG, "wrong-tag")).isNull(); + assertThat(countDistributionTotal(Metric.RULE, "SonarQube:rule-the-world")).isEqualTo(3); + assertThat(countDistributionTotal(Metric.RULE, "SonarQube:has-a-fake-rule")).isNull(); + assertThat(underTest.globalStatistics().effort().getTotal()).isEqualTo(15L); assertThat(underTest.globalStatistics().hasIssues()).isTrue(); assertThat(underTest.hasIssues()).isTrue(); assertThat(underTest.assigneesStatistics().get("maynard").hasIssues()).isTrue(); } @Test + public void add_counts_issue_per_severity_on_leak_globally_and_per_assignee() { + String assignee = randomAlphanumeric(10); + Severity.ALL.stream() + .map(severity -> new DefaultIssue().setSeverity(severity).setAssignee(assignee).setNew(true)) + .forEach(underTest::add); + + DistributedMetricStatsInt globalDistribution = underTest.globalStatistics().getDistributedMetricStats(Metric.SEVERITY); + DistributedMetricStatsInt assigneeDistribution = underTest.assigneesStatistics().get(assignee).getDistributedMetricStats(Metric.SEVERITY); + Stream.of(globalDistribution, assigneeDistribution) + .forEach(distribution -> { + assertStats(distribution, Severity.INFO, 1, 0, 1); + assertStats(distribution, Severity.MAJOR, 1, 0, 1); + assertStats(distribution, Severity.CRITICAL, 1, 0, 1); + assertStats(distribution, Severity.MINOR, 1, 0, 1); + assertStats(distribution, Severity.BLOCKER, 1, 0, 1); + }); + } + + @Test + public void add_counts_issue_per_severity_off_leak_globally_and_per_assignee() { + String assignee = randomAlphanumeric(10); + Severity.ALL.stream() + .map(severity -> new DefaultIssue().setSeverity(severity).setAssignee(assignee).setNew(false)) + .forEach(underTest::add); + + DistributedMetricStatsInt globalDistribution = underTest.globalStatistics().getDistributedMetricStats(Metric.SEVERITY); + DistributedMetricStatsInt assigneeDistribution = underTest.assigneesStatistics().get(assignee).getDistributedMetricStats(Metric.SEVERITY); + Stream.of(globalDistribution, assigneeDistribution) + .forEach(distribution -> { + assertStats(distribution, Severity.INFO, 0, 1, 1); + assertStats(distribution, Severity.MAJOR, 0, 1, 1); + assertStats(distribution, Severity.CRITICAL, 0, 1, 1); + assertStats(distribution, Severity.MINOR, 0, 1, 1); + assertStats(distribution, Severity.BLOCKER, 0, 1, 1); + }); + } + + @Test + public void add_counts_severity_if_null_globally_and_per_assignee_as_it_should_not_be_null() { + String assignee = randomAlphanumeric(10); + underTest.add(new DefaultIssue().setSeverity(null).setAssignee(assignee).setNew(new Random().nextBoolean())); + + DistributedMetricStatsInt globalDistribution = underTest.globalStatistics().getDistributedMetricStats(Metric.SEVERITY); + DistributedMetricStatsInt assigneeDistribution = underTest.assigneesStatistics().get(assignee).getDistributedMetricStats(Metric.SEVERITY); + Stream.of(globalDistribution, assigneeDistribution) + .forEach(distribution -> { + assertThat(distribution.getTotal()).isEqualTo(1); + assertThat(distribution.getForLabel(null).isPresent()).isTrue(); + }); + } + + @Test + public void add_counts_issue_per_component_on_leak_globally_and_per_assignee() { + List<String> componentUuids = IntStream.range(0, 1 + new Random().nextInt(10)).mapToObj(i -> randomAlphabetic(3)).collect(Collectors.toList()); + String assignee = randomAlphanumeric(10); + componentUuids.stream() + .map(componentUuid -> new DefaultIssue().setComponentUuid(componentUuid).setAssignee(assignee).setNew(true)) + .forEach(underTest::add); + + DistributedMetricStatsInt globalDistribution = underTest.globalStatistics().getDistributedMetricStats(Metric.COMPONENT); + DistributedMetricStatsInt assigneeDistribution = underTest.assigneesStatistics().get(assignee).getDistributedMetricStats(Metric.COMPONENT); + Stream.of(globalDistribution, assigneeDistribution) + .forEach(distribution -> componentUuids.forEach(componentUuid -> assertStats(distribution, componentUuid, 1, 0, 1))); + } + + @Test + public void add_counts_issue_per_component_off_leak_globally_and_per_assignee() { + List<String> componentUuids = IntStream.range(0, 1 + new Random().nextInt(10)).mapToObj(i -> randomAlphabetic(3)).collect(Collectors.toList()); + String assignee = randomAlphanumeric(10); + componentUuids.stream() + .map(componentUuid -> new DefaultIssue().setComponentUuid(componentUuid).setAssignee(assignee).setNew(false)) + .forEach(underTest::add); + + DistributedMetricStatsInt globalDistribution = underTest.globalStatistics().getDistributedMetricStats(Metric.COMPONENT); + NewIssuesStatistics.Stats stats = underTest.assigneesStatistics().get(assignee); + DistributedMetricStatsInt assigneeDistribution = stats.getDistributedMetricStats(Metric.COMPONENT); + Stream.of(globalDistribution, assigneeDistribution) + .forEach(distribution -> componentUuids.forEach(componentUuid -> assertStats(distribution, componentUuid, 0, 1, 1))); + } + + @Test + public void add_counts_component_if_null_globally_and_per_assignee_as_it_should_not_be_null() { + String assignee = randomAlphanumeric(10); + underTest.add(new DefaultIssue().setComponentUuid(null).setAssignee(assignee).setNew(new Random().nextBoolean())); + + DistributedMetricStatsInt globalDistribution = underTest.globalStatistics().getDistributedMetricStats(Metric.COMPONENT); + DistributedMetricStatsInt assigneeDistribution = underTest.assigneesStatistics().get(assignee).getDistributedMetricStats(Metric.COMPONENT); + Stream.of(globalDistribution, assigneeDistribution) + .forEach(distribution -> { + assertThat(distribution.getTotal()).isEqualTo(1); + assertThat(distribution.getForLabel(null).isPresent()).isTrue(); + }); + } + + @Test + public void add_counts_issue_per_ruleKey_on_leak_globally_and_per_assignee() { + String repository = randomAlphanumeric(3); + List<String> ruleKeys = IntStream.range(0, 1 + new Random().nextInt(10)).mapToObj(i -> randomAlphabetic(3)).collect(Collectors.toList()); + String assignee = randomAlphanumeric(10); + ruleKeys.stream() + .map(ruleKey -> new DefaultIssue().setRuleKey(RuleKey.of(repository, ruleKey)).setAssignee(assignee).setNew(true)) + .forEach(underTest::add); + + DistributedMetricStatsInt globalDistribution = underTest.globalStatistics().getDistributedMetricStats(Metric.RULE); + NewIssuesStatistics.Stats stats = underTest.assigneesStatistics().get(assignee); + DistributedMetricStatsInt assigneeDistribution = stats.getDistributedMetricStats(Metric.RULE); + Stream.of(globalDistribution, assigneeDistribution) + .forEach(distribution -> ruleKeys.forEach(ruleKey -> assertStats(distribution, RuleKey.of(repository, ruleKey).toString(), 1, 0, 1))); + } + + @Test + public void add_counts_issue_per_ruleKey_off_leak_globally_and_per_assignee() { + String repository = randomAlphanumeric(3); + List<String> ruleKeys = IntStream.range(0, 1 + new Random().nextInt(10)).mapToObj(i -> randomAlphabetic(3)).collect(Collectors.toList()); + String assignee = randomAlphanumeric(10); + ruleKeys.stream() + .map(ruleKey -> new DefaultIssue().setRuleKey(RuleKey.of(repository, ruleKey)).setAssignee(assignee).setNew(false)) + .forEach(underTest::add); + + DistributedMetricStatsInt globalDistribution = underTest.globalStatistics().getDistributedMetricStats(Metric.RULE); + DistributedMetricStatsInt assigneeDistribution = underTest.assigneesStatistics().get(assignee).getDistributedMetricStats(Metric.RULE); + Stream.of(globalDistribution, assigneeDistribution) + .forEach(distribution -> ruleKeys.forEach(ruleKey -> assertStats(distribution, RuleKey.of(repository, ruleKey).toString(), 0, 1, 1))); + } + + @Test + public void add_does_not_count_ruleKey_if_neither_neither_globally_nor_per_assignee() { + String assignee = randomAlphanumeric(10); + underTest.add(new DefaultIssue().setRuleKey(null).setAssignee(assignee).setNew(new Random().nextBoolean())); + + DistributedMetricStatsInt globalDistribution = underTest.globalStatistics().getDistributedMetricStats(Metric.RULE); + DistributedMetricStatsInt assigneeDistribution = underTest.assigneesStatistics().get(assignee).getDistributedMetricStats(Metric.RULE); + Stream.of(globalDistribution, assigneeDistribution) + .forEach(distribution -> { + assertThat(distribution.getTotal()).isEqualTo(0); + assertThat(distribution.getForLabel(null).isPresent()).isFalse(); + }); + } + + @Test + public void add_counts_issue_per_assignee_on_leak_globally_and_per_assignee() { + List<String> assignees = IntStream.range(0, 1 + new Random().nextInt(10)).mapToObj(i -> randomAlphabetic(3)).collect(Collectors.toList()); + assignees.stream() + .map(assignee -> new DefaultIssue().setAssignee(assignee).setNew(true)) + .forEach(underTest::add); + + DistributedMetricStatsInt globalDistribution = underTest.globalStatistics().getDistributedMetricStats(Metric.ASSIGNEE); + assignees.forEach(assignee -> assertStats(globalDistribution, assignee, 1, 0, 1)); + assignees.forEach(assignee -> { + NewIssuesStatistics.Stats stats = underTest.assigneesStatistics().get(assignee); + DistributedMetricStatsInt assigneeStats = stats.getDistributedMetricStats(Metric.ASSIGNEE); + assertThat(assigneeStats.getOnLeak()).isEqualTo(1); + assertThat(assigneeStats.getOffLeak()).isEqualTo(0); + assertThat(assigneeStats.getTotal()).isEqualTo(1); + assignees.forEach(s -> { + Optional<MetricStatsInt> forLabelOpts = assigneeStats.getForLabel(s); + if (s.equals(assignee)) { + assertThat(forLabelOpts.isPresent()).isTrue(); + MetricStatsInt forLabel = forLabelOpts.get(); + assertThat(forLabel.getOnLeak()).isEqualTo(1); + assertThat(forLabel.getOffLeak()).isEqualTo(0); + assertThat(forLabel.getTotal()).isEqualTo(1); + } else { + assertThat(forLabelOpts.isPresent()).isFalse(); + } + }); + }); + } + + @Test + public void add_counts_issue_per_assignee_off_leak_globally_and_per_assignee() { + List<String> assignees = IntStream.range(0, 1 + new Random().nextInt(10)).mapToObj(i -> randomAlphabetic(3)).collect(Collectors.toList()); + assignees.stream() + .map(assignee -> new DefaultIssue().setAssignee(assignee).setNew(false)) + .forEach(underTest::add); + + DistributedMetricStatsInt globalDistribution = underTest.globalStatistics().getDistributedMetricStats(Metric.ASSIGNEE); + assignees.forEach(assignee -> assertStats(globalDistribution, assignee, 0, 1, 1)); + assignees.forEach(assignee -> { + NewIssuesStatistics.Stats stats = underTest.assigneesStatistics().get(assignee); + DistributedMetricStatsInt assigneeStats = stats.getDistributedMetricStats(Metric.ASSIGNEE); + assertThat(assigneeStats.getOnLeak()).isEqualTo(0); + assertThat(assigneeStats.getOffLeak()).isEqualTo(1); + assertThat(assigneeStats.getTotal()).isEqualTo(1); + assignees.forEach(s -> { + Optional<MetricStatsInt> forLabelOpts = assigneeStats.getForLabel(s); + if (s.equals(assignee)) { + assertThat(forLabelOpts.isPresent()).isTrue(); + MetricStatsInt forLabel = forLabelOpts.get(); + assertThat(forLabel.getOnLeak()).isEqualTo(0); + assertThat(forLabel.getOffLeak()).isEqualTo(1); + assertThat(forLabel.getTotal()).isEqualTo(1); + } else { + assertThat(forLabelOpts.isPresent()).isFalse(); + } + }); + }); + } + + @Test + public void add_does_not_assignee_if_empty_neither_globally_nor_per_assignee() { + underTest.add(new DefaultIssue().setAssignee(null).setNew(new Random().nextBoolean())); + + DistributedMetricStatsInt globalDistribution = underTest.globalStatistics().getDistributedMetricStats(Metric.ASSIGNEE); + assertThat(globalDistribution.getTotal()).isEqualTo(0); + assertThat(globalDistribution.getForLabel(null).isPresent()).isFalse(); + assertThat(underTest.assigneesStatistics()).isEmpty(); + } + + @Test + public void add_counts_issue_per_tags_on_leak_globally_and_per_assignee() { + List<String> tags = IntStream.range(0, 1 + new Random().nextInt(10)).mapToObj(i -> randomAlphabetic(3)).collect(Collectors.toList()); + String assignee = randomAlphanumeric(10); + underTest.add(new DefaultIssue().setTags(tags).setAssignee(assignee).setNew(true)); + + DistributedMetricStatsInt globalDistribution = underTest.globalStatistics().getDistributedMetricStats(Metric.TAG); + DistributedMetricStatsInt assigneeDistribution = underTest.assigneesStatistics().get(assignee).getDistributedMetricStats(Metric.TAG); + Stream.of(globalDistribution, assigneeDistribution) + .forEach(distribution -> tags.forEach(tag -> assertStats(distribution, tag, 1, 0, 1))); + } + + @Test + public void add_counts_issue_per_tags_off_leak_globally_and_per_assignee() { + List<String> tags = IntStream.range(0, 1 + new Random().nextInt(10)).mapToObj(i -> randomAlphabetic(3)).collect(Collectors.toList()); + String assignee = randomAlphanumeric(10); + underTest.add(new DefaultIssue().setTags(tags).setAssignee(assignee).setNew(false)); + + DistributedMetricStatsInt globalDistribution = underTest.globalStatistics().getDistributedMetricStats(Metric.TAG); + DistributedMetricStatsInt assigneeDistribution = underTest.assigneesStatistics().get(assignee).getDistributedMetricStats(Metric.TAG); + Stream.of(globalDistribution, assigneeDistribution) + .forEach(distribution -> tags.forEach(tag -> assertStats(distribution, tag, 0, 1, 1))); + } + + @Test + public void add_does_not_count_tags_if_empty_neither_globally_nor_per_assignee() { + String assignee = randomAlphanumeric(10); + underTest.add(new DefaultIssue().setTags(Collections.emptyList()).setAssignee(assignee).setNew(new Random().nextBoolean())); + + DistributedMetricStatsInt globalDistribution = underTest.globalStatistics().getDistributedMetricStats(Metric.TAG); + DistributedMetricStatsInt assigneeDistribution = underTest.assigneesStatistics().get(assignee).getDistributedMetricStats(Metric.TAG); + Stream.of(globalDistribution, assigneeDistribution) + .forEach(distribution -> { + assertThat(distribution.getTotal()).isEqualTo(0); + assertThat(distribution.getForLabel(null).isPresent()).isFalse(); + }); + } + + @Test + public void add_sums_effort_on_leak_globally_and_per_assignee() { + Random random = new Random(); + List<Integer> efforts = IntStream.range(0, 1 + random.nextInt(10)).mapToObj(i -> 10_000 * i).collect(Collectors.toList()); + int expected = efforts.stream().mapToInt(s -> s).sum(); + String assignee = randomAlphanumeric(10); + efforts.stream() + .map(effort -> new DefaultIssue().setEffort(Duration.create(effort)).setAssignee(assignee).setNew(true)) + .forEach(underTest::add); + + MetricStatsLong globalDistribution = underTest.globalStatistics().effort(); + MetricStatsLong assigneeDistribution = underTest.assigneesStatistics().get(assignee).effort(); + Stream.of(globalDistribution, assigneeDistribution) + .forEach(distribution -> { + assertThat(distribution.getOnLeak()).isEqualTo(expected); + assertThat(distribution.getOffLeak()).isEqualTo(0); + assertThat(distribution.getTotal()).isEqualTo(expected); + }); + } + + @Test + public void add_sums_effort_off_leak_globally_and_per_assignee() { + Random random = new Random(); + List<Integer> efforts = IntStream.range(0, 1 + random.nextInt(10)).mapToObj(i -> 10_000 * i).collect(Collectors.toList()); + int expected = efforts.stream().mapToInt(s -> s).sum(); + String assignee = randomAlphanumeric(10); + efforts.stream() + .map(effort -> new DefaultIssue().setEffort(Duration.create(effort)).setAssignee(assignee).setNew(false)) + .forEach(underTest::add); + + MetricStatsLong globalDistribution = underTest.globalStatistics().effort(); + MetricStatsLong assigneeDistribution = underTest.assigneesStatistics().get(assignee).effort(); + Stream.of(globalDistribution, assigneeDistribution) + .forEach(distribution -> { + assertThat(distribution.getOnLeak()).isEqualTo(0); + assertThat(distribution.getOffLeak()).isEqualTo(expected); + assertThat(distribution.getTotal()).isEqualTo(expected); + }); + } + + @Test + public void add_does_not_sum_effort_if_null_neither_globally_nor_per_assignee() { + String assignee = randomAlphanumeric(10); + underTest.add(new DefaultIssue().setEffort(null).setAssignee(assignee).setNew(new Random().nextBoolean())); + + MetricStatsLong globalDistribution = underTest.globalStatistics().effort(); + MetricStatsLong assigneeDistribution = underTest.assigneesStatistics().get(assignee).effort(); + Stream.of(globalDistribution, assigneeDistribution) + .forEach(distribution -> assertThat(distribution.getTotal()).isEqualTo(0)); + } + + private void assertStats(DistributedMetricStatsInt distribution, String label, int onLeak, int offLeak, int total) { + Optional<MetricStatsInt> statsOption = distribution.getForLabel(label); + assertThat(statsOption.isPresent()).describedAs("distribution for label %s not found", label).isTrue(); + MetricStatsInt stats = statsOption.get(); + assertThat(stats.getOnLeak()).isEqualTo(onLeak); + assertThat(stats.getOffLeak()).isEqualTo(offLeak); + assertThat(stats.getTotal()).isEqualTo(total); + } + + @Test + public void add_counts_issue_per_severity_per_assignee() { + String assignee = randomAlphanumeric(20); + Severity.ALL.stream().map(severity -> new DefaultIssue() + .setSeverity(severity) + .setAssignee(assignee)).forEach(underTest::add); + + assertThat(underTest.globalStatistics() + .getDistributedMetricStats(Metric.SEVERITY) + .getForLabel(Severity.INFO) + .map(MetricStatsInt::getTotal) + .orElse(null)).isEqualTo(1); + assertThat(countDistributionTotal(Metric.SEVERITY, Severity.MINOR)).isEqualTo(1); + assertThat(countDistributionTotal(Metric.SEVERITY, Severity.CRITICAL)).isEqualTo(1); + assertThat(countDistributionTotal(Metric.SEVERITY, Severity.BLOCKER)).isEqualTo(1); + assertThat(countDistributionTotal(Metric.SEVERITY, Severity.MAJOR)).isEqualTo(1); + } + + @Test public void do_not_have_issues_when_no_issue_added() { assertThat(underTest.globalStatistics().hasIssues()).isFalse(); } - private int countDistribution(Metric metric, String label) { - return underTest.globalStatistics().countForMetric(metric, label); + @CheckForNull + private Integer countDistributionTotal(Metric metric, String label) { + return underTest.globalStatistics() + .getDistributedMetricStats(metric) + .getForLabel(label) + .map(MetricStatsInt::getTotal) + .orElse(null); + } + + @CheckForNull + private Integer countDistributionOnLeak(Metric metric, String label) { + return underTest.globalStatistics() + .getDistributedMetricStats(metric) + .getForLabel(label) + .map(MetricStatsInt::getOnLeak) + .orElse(null); + } + + @CheckForNull + private Integer countDistributionOffLeak(Metric metric, String label) { + return underTest.globalStatistics() + .getDistributedMetricStats(metric) + .getForLabel(label) + .map(MetricStatsInt::getOffLeak) + .orElse(null); } private DefaultIssue defaultIssue() { |