]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9144 support leak period when computing issue statistics
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Tue, 12 Sep 2017 10:37:15 +0000 (12:37 +0200)
committerEric Hartmann <hartmann.eric@gmail.Com>
Mon, 2 Oct 2017 11:03:35 +0000 (13:03 +0200)
during report processing

12 files changed:
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/SendIssueNotificationsStep.java
server/sonar-server/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java
server/sonar-server/src/main/java/org/sonar/server/issue/notification/DistributedMetricStatsInt.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/issue/notification/MetricStatsInt.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/issue/notification/MetricStatsLong.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotification.java
server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesStatistics.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/SendIssueNotificationsStepTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesStatisticsTest.java

index 6e26dcbad0741a054e967a066b00f473c491a072..cd1dde632dd604f43df3213251ac39d1dfa8eac9 100644 (file)
@@ -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);
     }
index caecaa2851e9a1da5f4b53d3ae82a1f7a6378289..8b66c747288bfe6c054be6148a351d431dda9886 100644 (file)
@@ -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 (file)
index 0000000..b0a3312
--- /dev/null
@@ -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 (file)
index 0000000..f1d546c
--- /dev/null
@@ -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 (file)
index 0000000..ae43cf4
--- /dev/null
@@ -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;
+  }
+
+}
index 69a91c20081266f906623129d29c51dca0dbe2a3..6d2df27822ed07e9faaf6e9bf9f8c0f4c5d4f78d 100644 (file)
  */
 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)));
     }
   }
 
index b4746bed5f586203fc5423f1db1acd1b92e30fdb..8410d8a9caaf8a8c979cc65918b807a21fc223d6 100644 (file)
  */
 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);
-    }
   }
+
 }
index 824c72a912926eaad9a4e2081a7685f8e3cfcdbb..384f05314cb64a78cbd585e47d7f61047b77481d 100644 (file)
@@ -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)
index 8922e3fe0c634c3e0c4d026cd4c9f7cfa9c3f257..5bd31513272ff3d5c9b113fa7682d9dedef100ff 100644 (file)
@@ -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")
index b2116b238fde2ac698040b7a5d994972d7db71ff..7c8fce8dcdd998117f645f8e36abf42bdf62ac8d 100644 (file)
@@ -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")
index 1c62a3c083cf2fbef6a9bc7d9b1670bad7a4afeb..f5ff4dd9a432f956b367a3e4ff2b8d60d627c1d6 100644 (file)
@@ -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) {
index 8642b392be8b18fc414a89a6a0007a00ff6d8835..6a56f8de7d00432ff10c4203ae742b81e8c55468 100644 (file)
 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() {