]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9144 add coverage on 5 elements per category + SQL optimisation
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Wed, 13 Sep 2017 15:42:20 +0000 (17:42 +0200)
committerEric Hartmann <hartmann.eric@gmail.Com>
Mon, 2 Oct 2017 11:03:35 +0000 (13:03 +0200)
server/sonar-server/src/main/java/org/sonar/server/issue/notification/DistributedMetricStatsInt.java
server/sonar-server/src/main/java/org/sonar/server/issue/notification/MetricStatsInt.java
server/sonar-server/src/main/java/org/sonar/server/issue/notification/MetricStatsLong.java
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/issue/notification/NewIssuesNotificationTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesStatisticsTest.java

index b0a331270f49e5d64682bc4a8bf3ec88eeb08e35..1bd134372091ba2bf12e5b746d3ee4e9f11c2d45 100644 (file)
@@ -54,4 +54,11 @@ public class DistributedMetricStatsInt {
     return globalStats.getTotal();
   }
 
+  @Override
+  public String toString() {
+    return "DistributedMetricStatsInt{" +
+      "globalStats=" + globalStats +
+      ", statsPerLabel=" + statsPerLabel +
+      '}';
+  }
 }
index f1d546c5293b4dff87d3b51c8a1ff70a1b41287b..4ed69a0758a1500fad5631c44ea58fabc2e1ff83 100644 (file)
@@ -44,4 +44,11 @@ public class MetricStatsInt {
     return onLeak + offLeak;
   }
 
+  @Override
+  public String toString() {
+    return "MetricStatsInt{" +
+      "onLeak=" + onLeak +
+      ", offLeak=" + offLeak +
+      '}';
+  }
 }
index ae43cf4ee2ada2b287d6bf52926d31d431f55208..442fdbcac2181180b6cec2506af004010ee2a510 100644 (file)
@@ -44,4 +44,11 @@ public class MetricStatsLong {
     return onLeak + offLeak;
   }
 
+  @Override
+  public String toString() {
+    return "MetricStatsLong{" +
+      "onLeak=" + onLeak +
+      ", offLeak=" + offLeak +
+      '}';
+  }
 }
index 8d26d8d518791ec58935e84a9865ba37ea1908f5..7475b14c97fce3db25289eabe0987d61329db90c 100644 (file)
  */
 package org.sonar.server.issue.notification;
 
+import com.google.common.collect.ImmutableMap;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
 import java.util.function.ToIntFunction;
 import javax.annotation.Nullable;
 import org.sonar.api.notifications.Notification;
@@ -34,6 +37,8 @@ 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.RowNotFoundException;
+import org.sonar.db.component.ComponentDto;
 import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.server.issue.notification.NewIssuesStatistics.Metric;
 import org.sonar.server.user.index.UserDoc;
@@ -100,10 +105,20 @@ public class NewIssuesNotification extends Notification {
 
   private void setRuleStatistics(DbSession dbSession, NewIssuesStatistics.Stats stats) {
     Metric metric = Metric.RULE;
+    List<Map.Entry<String, MetricStatsInt>> fiveBiggest = fiveBiggest(stats.getDistributedMetricStats(metric), MetricStatsInt::getOnLeak);
+    Set<RuleKey> ruleKeys = fiveBiggest
+      .stream()
+      .map(Map.Entry::getKey)
+      .map(RuleKey::parse)
+      .collect(MoreCollectors.toSet(fiveBiggest.size()));
+    ImmutableMap<String, RuleDefinitionDto> ruleByRuleKey = dbClient.ruleDao().selectDefinitionByKeys(dbSession, ruleKeys)
+      .stream()
+      .collect(MoreCollectors.uniqueIndex(s -> s.getKey().toString()));
     int i = 1;
-    for (Map.Entry<String, MetricStatsInt> ruleStats : fiveBiggest(stats.getDistributedMetricStats(metric), MetricStatsInt::getOnLeak)) {
+    for (Map.Entry<String, MetricStatsInt> ruleStats : fiveBiggest) {
       String ruleKey = ruleStats.getKey();
-      RuleDefinitionDto rule = dbClient.ruleDao().selectOrFailDefinitionByKey(dbSession, RuleKey.parse(ruleKey));
+      RuleDefinitionDto rule = Optional.ofNullable(ruleByRuleKey.get(ruleKey))
+        .orElseThrow(() -> new RowNotFoundException(String.format("Rule with key '%s' does not exist", ruleKey)));
       String name = rule.getName() + " (" + rule.getLanguage() + ")";
       setFieldValue(metric + DOT + i + LABEL, name);
       setFieldValue(metric + DOT + i + COUNT, String.valueOf(ruleStats.getValue().getOnLeak()));
@@ -114,9 +129,19 @@ public class NewIssuesNotification extends Notification {
   private void setComponentsStatistics(DbSession dbSession, NewIssuesStatistics.Stats stats) {
     Metric metric = Metric.COMPONENT;
     int i = 1;
-    for (Map.Entry<String, MetricStatsInt> componentStats : fiveBiggest(stats.getDistributedMetricStats(metric), MetricStatsInt::getOnLeak)) {
+    List<Map.Entry<String, MetricStatsInt>> fiveBiggest = fiveBiggest(stats.getDistributedMetricStats(metric), MetricStatsInt::getOnLeak);
+    Set<String> componentUuids = fiveBiggest
+      .stream()
+      .map(Map.Entry::getKey)
+      .collect(MoreCollectors.toSet(fiveBiggest.size()));
+    Map<String, ComponentDto> componentDtosByUuid = dbClient.componentDao().selectByUuids(dbSession, componentUuids)
+      .stream()
+      .collect(MoreCollectors.uniqueIndex(ComponentDto::uuid));
+    for (Map.Entry<String, MetricStatsInt> componentStats : fiveBiggest) {
       String uuid = componentStats.getKey();
-      String componentName = dbClient.componentDao().selectOrFailByUuid(dbSession, uuid).name();
+      String componentName = Optional.ofNullable(componentDtosByUuid.get(uuid))
+        .map(ComponentDto::name)
+        .orElseThrow(() -> new RowNotFoundException(String.format("Component with uuid '%s' not found", uuid)));
       setFieldValue(metric + DOT + i + LABEL, componentName);
       setFieldValue(metric + DOT + i + COUNT, String.valueOf(componentStats.getValue().getOnLeak()));
       i++;
index 8101f45aeb21aef87b5c2749abbd2d402fbc752f..3d39a6ccca973dfa7d13aab7e0467ac0b4c19575 100644 (file)
@@ -84,6 +84,14 @@ public class NewIssuesStatistics {
     }
   }
 
+  @Override
+  public String toString() {
+    return "NewIssuesStatistics{" +
+      "assigneesStatistics=" + assigneesStatistics +
+      ", globalStatistics=" + globalStatistics +
+      '}';
+  }
+
   public static class Stats {
     private final Predicate<Issue> onLeakPredicate;
     private final Map<Metric, DistributedMetricStatsInt> distributions = new EnumMap<>(Metric.class);
@@ -101,7 +109,10 @@ public class NewIssuesStatistics {
     public void add(Issue issue) {
       boolean isOnLeak = onLeakPredicate.test(issue);
       distributions.get(SEVERITY).increment(issue.severity(), isOnLeak);
-      distributions.get(COMPONENT).increment(issue.componentUuid(), isOnLeak);
+      String componentUuid = issue.componentUuid();
+      if (componentUuid != null) {
+        distributions.get(COMPONENT).increment(componentUuid, isOnLeak);
+      }
       RuleKey ruleKey = issue.ruleKey();
       if (ruleKey != null) {
         distributions.get(RULE).increment(ruleKey.toString(), isOnLeak);
@@ -139,6 +150,13 @@ public class NewIssuesStatistics {
       return getDistributedMetricStats(SEVERITY).getOffLeak() > 0;
     }
 
+    @Override
+    public String toString() {
+      return "Stats{" +
+        "distributions=" + distributions +
+        ", effortStats=" + effortStats +
+        '}';
+    }
   }
 
 }
index f5ff4dd9a432f956b367a3e4ff2b8d60d627c1d6..430c3b056bac890f94d45dafa00e23a80aed612f 100644 (file)
  */
 package org.sonar.server.issue.notification;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.Date;
+import java.util.Random;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.junit.Before;
 import org.junit.Test;
-import org.mockito.Mockito;
 import org.sonar.api.rule.RuleKey;
 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.issue.DefaultIssue;
+import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDao;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.rule.RuleDao;
 import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.server.user.index.UserIndex;
 
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyCollection;
+import static org.mockito.Matchers.same;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.ASSIGNEE;
@@ -48,11 +62,22 @@ import static org.sonar.server.issue.notification.NewIssuesStatistics.Metric.TAG
 
 public class NewIssuesNotificationTest {
 
-  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);
-  NewIssuesNotification underTest = new NewIssuesNotification(userIndex, dbClient, durations);
+  private NewIssuesStatistics.Stats stats = new NewIssuesStatistics.Stats(i -> true);
+  private UserIndex userIndex = mock(UserIndex.class);
+  private DbClient dbClient = mock(DbClient.class);
+  private DbSession dbSession = mock(DbSession.class);
+  private ComponentDao componentDao = mock(ComponentDao.class);
+  private RuleDao ruleDao = mock(RuleDao.class);
+  private Durations durations = mock(Durations.class);
+  private NewIssuesNotification underTest = new NewIssuesNotification(userIndex, dbClient, durations);
+
+  @Before
+  public void setUp() throws Exception {
+    when(dbClient.openSession(anyBoolean())).thenReturn(dbSession);
+    when(dbClient.componentDao()).thenReturn(componentDao);
+    when(dbClient.ruleDao()).thenReturn(ruleDao);
+    when(componentDao.selectByUuids(same(dbSession), anyCollection())).thenReturn(Collections.emptyList());
+  }
 
   @Test
   public void set_project_without_branch() {
@@ -87,10 +112,15 @@ public class NewIssuesNotificationTest {
   public void set_statistics() {
     addIssueNTimes(newIssue1(), 5);
     addIssueNTimes(newIssue2(), 3);
-    when(dbClient.componentDao().selectOrFailByUuid(any(DbSession.class), eq("file-uuid")).name()).thenReturn("file-name");
-    when(dbClient.componentDao().selectOrFailByUuid(any(DbSession.class), eq("directory-uuid")).name()).thenReturn("directory-name");
-    when(dbClient.ruleDao().selectOrFailDefinitionByKey(any(DbSession.class), eq(RuleKey.of("SonarQube", "rule-the-world")))).thenReturn(newRule("Rule the World", "Java"));
-    when(dbClient.ruleDao().selectOrFailDefinitionByKey(any(DbSession.class), eq(RuleKey.of("SonarQube", "rule-the-universe")))).thenReturn(newRule("Rule the Universe", "Clojure"));
+    when(componentDao.selectByUuids(dbSession, ImmutableSet.of("file-uuid", "directory-uuid")))
+      .thenReturn(Arrays.asList(
+        new ComponentDto().setUuid("file-uuid").setName("file-name"),
+        new ComponentDto().setUuid("directory-uuid").setName("directory-name")));
+    RuleKey rule1 = RuleKey.of("SonarQube", "rule-the-world");
+    RuleKey rule2 = RuleKey.of("SonarQube", "rule-the-universe");
+    when(ruleDao.selectDefinitionByKeys(dbSession, ImmutableSet.of(rule1, rule2)))
+      .thenReturn(
+        ImmutableList.of(newRule(rule1, "Rule the World", "Java"), newRule(rule2, "Rule the Universe", "Clojure")));
 
     underTest.setStatistics("project-long-name", stats);
 
@@ -115,6 +145,83 @@ public class NewIssuesNotificationTest {
     assertThat(underTest.getDefaultMessage()).startsWith("8 new issues on project-long-name");
   }
 
+  @Test
+  public void add_only_5_assignees_with_biggest_issue_counts() {
+    Random random = new Random();
+    String[] assignees = IntStream.range(0, 6 + random.nextInt(10)).mapToObj(s -> "assignee" + s).toArray(String[]::new);
+    NewIssuesStatistics.Stats stats = new NewIssuesStatistics.Stats(i -> true);
+    int i = assignees.length;
+    for (String assignee : assignees) {
+      IntStream.range(0, i).mapToObj(j -> new DefaultIssue().setAssignee(assignee)).forEach(stats::add);
+      i--;
+    }
+
+    underTest.setStatistics(randomAlphanumeric(20), stats);
+
+    for (int j = 0; j < 5; j++) {
+      String fieldBase = ASSIGNEE + "." + (j + 1);
+      assertThat(underTest.getFieldValue(fieldBase + ".label")).as("label of %s", fieldBase).isEqualTo(assignees[j]);
+      assertThat(underTest.getFieldValue(fieldBase + ".count")).as("count of %s", fieldBase).isEqualTo(String.valueOf(assignees.length - j));
+    }
+    assertThat(underTest.getFieldValue(ASSIGNEE + ".6.label")).isNull();
+    assertThat(underTest.getFieldValue(ASSIGNEE + ".6.count")).isNull();
+  }
+
+  @Test
+  public void add_only_5_components_with_biggest_issue_counts() {
+    Random random = new Random();
+    String[] componentUuids = IntStream.range(0, 6 + random.nextInt(10)).mapToObj(s -> "component_uuid_" + s).toArray(String[]::new);
+    NewIssuesStatistics.Stats stats = new NewIssuesStatistics.Stats(i -> true);
+    int i = componentUuids.length;
+    for (String component : componentUuids) {
+      IntStream.range(0, i).mapToObj(j -> new DefaultIssue().setComponentUuid(component)).forEach(stats::add);
+      i--;
+    }
+    when(componentDao.selectByUuids(dbSession, Arrays.stream(componentUuids).limit(5).collect(Collectors.toSet())))
+      .thenReturn(
+        Arrays.stream(componentUuids).map(uuid -> new ComponentDto().setUuid(uuid).setName("name_" + uuid)).collect(MoreCollectors.toList()));
+
+    underTest.setStatistics(randomAlphanumeric(20), stats);
+
+    for (int j = 0; j < 5; j++) {
+      String fieldBase = COMPONENT + "." + (j + 1);
+      assertThat(underTest.getFieldValue(fieldBase + ".label")).as("label of %s", fieldBase).isEqualTo("name_" + componentUuids[j]);
+      assertThat(underTest.getFieldValue(fieldBase + ".count")).as("count of %s", fieldBase).isEqualTo(String.valueOf(componentUuids.length - j));
+    }
+    assertThat(underTest.getFieldValue(COMPONENT + ".6.label")).isNull();
+    assertThat(underTest.getFieldValue(COMPONENT + ".6.count")).isNull();
+  }
+
+  @Test
+  public void add_only_5_rules_with_biggest_issue_counts() {
+    Random random = new Random();
+    String repository = randomAlphanumeric(4);
+    String[] ruleKeys = IntStream.range(0, 6 + random.nextInt(10)).mapToObj(s -> "rule_" + s).toArray(String[]::new);
+    NewIssuesStatistics.Stats stats = new NewIssuesStatistics.Stats(i -> true);
+    int i = ruleKeys.length;
+    for (String ruleKey : ruleKeys) {
+      IntStream.range(0, i).mapToObj(j -> new DefaultIssue().setRuleKey(RuleKey.of(repository, ruleKey))).forEach(stats::add);
+      i--;
+    }
+    when(ruleDao.selectDefinitionByKeys(dbSession, Arrays.stream(ruleKeys).limit(5).map(s -> RuleKey.of(repository, s)).collect(MoreCollectors.toSet(5))))
+      .thenReturn(
+        Arrays.stream(ruleKeys).limit(5).map(ruleKey -> new RuleDefinitionDto()
+          .setRuleKey(RuleKey.of(repository, ruleKey))
+          .setName("name_" + ruleKey)
+          .setLanguage("language_" + ruleKey))
+          .collect(MoreCollectors.toList(5)));
+
+    underTest.setStatistics(randomAlphanumeric(20), stats);
+
+    for (int j = 0; j < 5; j++) {
+      String fieldBase = RULE + "." + (j + 1);
+      assertThat(underTest.getFieldValue(fieldBase + ".label")).as("label of %s", fieldBase).isEqualTo("name_" + ruleKeys[j] + " (language_" + ruleKeys[j] + ")");
+      assertThat(underTest.getFieldValue(fieldBase + ".count")).as("count of %s", fieldBase).isEqualTo(String.valueOf(ruleKeys.length - j));
+    }
+    assertThat(underTest.getFieldValue(RULE + ".6.label")).isNull();
+    assertThat(underTest.getFieldValue(RULE + ".6.count")).isNull();
+  }
+
   @Test
   public void set_debt() {
     when(durations.format(any(Duration.class))).thenReturn("55 min");
@@ -150,8 +257,9 @@ public class NewIssuesNotificationTest {
       .setEffort(Duration.create(10L));
   }
 
-  private RuleDefinitionDto newRule(String name, String language) {
+  private RuleDefinitionDto newRule(RuleKey ruleKey, String name, String language) {
     return new RuleDefinitionDto()
+      .setRuleKey(ruleKey)
       .setName(name)
       .setLanguage(language);
   }
index b8417cf04e2f1de9c357a412606be102978b346b..0637551a81a234bcf1affbde65b3cdfa04bf9895 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.server.issue.notification;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import java.util.Collections;
 import java.util.List;
@@ -159,7 +160,7 @@ public class NewIssuesStatisticsTest {
   }
 
   @Test
-  public void add_counts_component_if_null_globally_and_per_assignee_as_it_should_not_be_null() {
+  public void add_does_not_count_component_if_null_neither_globally_nor_per_assignee() {
     String assignee = randomAlphanumeric(10);
     underTest.add(new DefaultIssue().setComponentUuid(null).setAssignee(assignee).setNew(new Random().nextBoolean()));
 
@@ -167,8 +168,8 @@ public class NewIssuesStatisticsTest {
     DistributedMetricStatsInt assigneeDistribution = underTest.getAssigneesStatistics().get(assignee).getDistributedMetricStats(Metric.COMPONENT);
     Stream.of(globalDistribution, assigneeDistribution)
       .forEach(distribution -> {
-        assertThat(distribution.getTotal()).isEqualTo(1);
-        assertThat(distribution.getForLabel(null).isPresent()).isTrue();
+        assertThat(distribution.getTotal()).isEqualTo(0);
+        assertThat(distribution.getForLabel(null).isPresent()).isFalse();
       });
   }
 
@@ -204,7 +205,7 @@ public class NewIssuesStatisticsTest {
   }
 
   @Test
-  public void add_does_not_count_ruleKey_if_neither_neither_globally_nor_per_assignee() {
+  public void add_does_not_count_ruleKey_if_null_neither_globally_nor_per_assignee() {
     String assignee = randomAlphanumeric(10);
     underTest.add(new DefaultIssue().setRuleKey(null).setAssignee(assignee).setNew(new Random().nextBoolean()));
 
@@ -376,15 +377,6 @@ public class NewIssuesStatisticsTest {
       .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);
@@ -408,41 +400,66 @@ public class NewIssuesStatisticsTest {
     assertThat(underTest.globalStatistics().hasIssues()).isFalse();
   }
 
-  @CheckForNull
-  private Integer countDistributionTotal(Metric metric, String label) {
-    return underTest.globalStatistics()
-      .getDistributedMetricStats(metric)
-      .getForLabel(label)
-      .map(MetricStatsInt::getTotal)
-      .orElse(null);
+  @Test
+  public void verify_toString() {
+    String componentUuid = randomAlphanumeric(2);
+    String tag = randomAlphanumeric(3);
+    String assignee = randomAlphanumeric(4);
+    int effort = 10 + new Random().nextInt(5);
+    RuleKey ruleKey = RuleKey.of(randomAlphanumeric(5), randomAlphanumeric(6));
+    underTest.add(new DefaultIssue()
+      .setSeverity(Severity.BLOCKER)
+      .setComponentUuid(componentUuid)
+      .setTags(ImmutableSet.of(tag))
+      .setAssignee(assignee)
+      .setRuleKey(ruleKey)
+      .setEffort(Duration.create(effort)));
+
+    assertThat(underTest.toString())
+      .isEqualTo("NewIssuesStatistics{" +
+        "assigneesStatistics={" + assignee + "=" +
+        "Stats{distributions={" +
+        "SEVERITY=DistributedMetricStatsInt{globalStats=MetricStatsInt{onLeak=1, offLeak=0}, " +
+        "statsPerLabel={" + Severity.BLOCKER + "=MetricStatsInt{onLeak=1, offLeak=0}}}, " +
+        "TAG=DistributedMetricStatsInt{globalStats=MetricStatsInt{onLeak=1, offLeak=0}, " +
+        "statsPerLabel={" + tag + "=MetricStatsInt{onLeak=1, offLeak=0}}}, " +
+        "COMPONENT=DistributedMetricStatsInt{globalStats=MetricStatsInt{onLeak=1, offLeak=0}, " +
+        "statsPerLabel={" + componentUuid + "=MetricStatsInt{onLeak=1, offLeak=0}}}, " +
+        "ASSIGNEE=DistributedMetricStatsInt{globalStats=MetricStatsInt{onLeak=1, offLeak=0}, " +
+        "statsPerLabel={" + assignee + "=MetricStatsInt{onLeak=1, offLeak=0}}}, " +
+        "RULE=DistributedMetricStatsInt{globalStats=MetricStatsInt{onLeak=1, offLeak=0}, " +
+        "statsPerLabel={" + ruleKey.toString() + "=MetricStatsInt{onLeak=1, offLeak=0}}}}, " +
+        "effortStats=MetricStatsLong{onLeak=" + effort + ", offLeak=0}}}, " +
+        "globalStatistics=Stats{distributions={" +
+        "SEVERITY=DistributedMetricStatsInt{globalStats=MetricStatsInt{onLeak=1, offLeak=0}, " +
+        "statsPerLabel={" + Severity.BLOCKER + "=MetricStatsInt{onLeak=1, offLeak=0}}}, " +
+        "TAG=DistributedMetricStatsInt{globalStats=MetricStatsInt{onLeak=1, offLeak=0}, " +
+        "statsPerLabel={" + tag + "=MetricStatsInt{onLeak=1, offLeak=0}}}, " +
+        "COMPONENT=DistributedMetricStatsInt{globalStats=MetricStatsInt{onLeak=1, offLeak=0}, " +
+        "statsPerLabel={" + componentUuid + "=MetricStatsInt{onLeak=1, offLeak=0}}}, " +
+        "ASSIGNEE=DistributedMetricStatsInt{globalStats=MetricStatsInt{onLeak=1, offLeak=0}, " +
+        "statsPerLabel={" + assignee + "=MetricStatsInt{onLeak=1, offLeak=0}}}, " +
+        "RULE=DistributedMetricStatsInt{globalStats=MetricStatsInt{onLeak=1, offLeak=0}, " +
+        "statsPerLabel={" + ruleKey.toString() + "=MetricStatsInt{onLeak=1, offLeak=0}}}}, " +
+        "effortStats=MetricStatsLong{onLeak=" + effort + ", offLeak=0}}}");
   }
 
   @CheckForNull
-  private Integer countDistributionOnLeak(Metric metric, String label) {
+  private Integer countDistributionTotal(Metric metric, String label) {
     return underTest.globalStatistics()
       .getDistributedMetricStats(metric)
       .getForLabel(label)
-      .map(MetricStatsInt::getOnLeak)
+      .map(MetricStatsInt::getTotal)
       .orElse(null);
   }
 
-  @CheckForNull
-  private Integer countDistributionOffLeak(Metric metric, String label) {
-    return underTest.globalStatistics()
-      .getDistributedMetricStats(metric)
-      .getForLabel(label)
-      .map(MetricStatsInt::getOffLeak)
-      .orElse(null);
+  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);
   }
 
-  private DefaultIssue defaultIssue() {
-    return 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));
-  }
 }