]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6646 Compute measures on issues 385/head
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Fri, 19 Jun 2015 12:49:52 +0000 (14:49 +0200)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Mon, 22 Jun 2015 10:45:26 +0000 (12:45 +0200)
server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java
server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputeIssueMeasuresStep.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/step/ComputeIssueMeasuresStepTest.java [new file with mode: 0644]

index 043b0ddf8be8a0fa2db7025e7299596457a364a4..dcbb8a2b200ad942f2952b0d367ee94a199ea8aa 100644 (file)
@@ -43,6 +43,7 @@ public class ComputationSteps {
       BuildComponentTreeStep.class,
       FillComponentsStep.class,
       ValidateProjectStep.class,
+
       FeedDebtModelStep.class,
 
       // Read report
@@ -62,7 +63,8 @@ public class ComputationSteps {
       QualityGateMeasuresStep.class,
       
       QualityProfileEventsStep.class,
-      
+
+      ComputeIssueMeasuresStep.class,
       // Must be executed after computation of quality gate measure
       QualityGateEventsStep.class,
 
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputeIssueMeasuresStep.java b/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputeIssueMeasuresStep.java
new file mode 100644 (file)
index 0000000..1fd3058
--- /dev/null
@@ -0,0 +1,341 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.computation.step;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multiset;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.sonar.api.rule.Severity;
+import org.sonar.batch.protocol.output.BatchReport.Issue;
+import org.sonar.server.computation.batch.BatchReportReader;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.DepthTraversalTypeAwareVisitor;
+import org.sonar.server.computation.component.TreeRootHolder;
+import org.sonar.server.computation.measure.Measure;
+import org.sonar.server.computation.measure.MeasureRepository;
+import org.sonar.server.computation.measure.MeasureVariations;
+import org.sonar.server.computation.metric.Metric;
+import org.sonar.server.computation.metric.MetricRepository;
+import org.sonar.server.computation.period.Period;
+import org.sonar.server.computation.period.PeriodsHolder;
+
+import static com.google.common.collect.FluentIterable.from;
+import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
+import static org.sonar.api.issue.Issue.STATUS_CONFIRMED;
+import static org.sonar.api.issue.Issue.STATUS_OPEN;
+import static org.sonar.api.issue.Issue.STATUS_REOPENED;
+import static org.sonar.api.measures.CoreMetrics.BLOCKER_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.CONFIRMED_ISSUES_KEY;
+import static org.sonar.api.measures.CoreMetrics.CRITICAL_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.FALSE_POSITIVE_ISSUES_KEY;
+import static org.sonar.api.measures.CoreMetrics.INFO_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.MAJOR_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.MINOR_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_BLOCKER_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_CRITICAL_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_INFO_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_MAJOR_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_MINOR_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.OPEN_ISSUES_KEY;
+import static org.sonar.api.measures.CoreMetrics.REOPENED_ISSUES_KEY;
+import static org.sonar.api.measures.CoreMetrics.VIOLATIONS_KEY;
+import static org.sonar.server.computation.component.Component.Type.FILE;
+import static org.sonar.server.computation.component.DepthTraversalTypeAwareVisitor.Order.POST_ORDER;
+
+/**
+ * Computes metrics related to number of issues.
+ * - Total number of issues and new issues
+ * - Number of issues by severity, and new issues by severity
+ * - Number of false-positives
+ */
+public class ComputeIssueMeasuresStep implements ComputationStep {
+
+  private final BatchReportReader reportReader;
+  private final TreeRootHolder treeRootHolder;
+  private final PeriodsHolder periodsHolder;
+  private final MeasureRepository measureRepository;
+  private final MetricRepository metricRepository;
+
+  private final static Map<String, String> SEVERITY_METRIC_KEY_BY_SEVERITY = ImmutableMap.of(
+    Severity.BLOCKER, BLOCKER_VIOLATIONS_KEY,
+    Severity.CRITICAL, CRITICAL_VIOLATIONS_KEY,
+    Severity.MAJOR, MAJOR_VIOLATIONS_KEY,
+    Severity.MINOR, MINOR_VIOLATIONS_KEY,
+    Severity.INFO, INFO_VIOLATIONS_KEY
+  );
+
+  private final static Map<String, String> NEW_SEVERITY_METRIC_KEY_BY_SEVERITY = ImmutableMap.of(
+    Severity.BLOCKER, NEW_BLOCKER_VIOLATIONS_KEY,
+    Severity.CRITICAL, NEW_CRITICAL_VIOLATIONS_KEY,
+    Severity.MAJOR, NEW_MAJOR_VIOLATIONS_KEY,
+    Severity.MINOR, NEW_MINOR_VIOLATIONS_KEY,
+    Severity.INFO, NEW_INFO_VIOLATIONS_KEY
+  );
+
+  public ComputeIssueMeasuresStep(PeriodsHolder periodsHolder, BatchReportReader reportReader, TreeRootHolder treeRootHolder, MeasureRepository measureRepository,
+                                  MetricRepository metricRepository) {
+    this.periodsHolder = periodsHolder;
+    this.reportReader = reportReader;
+    this.treeRootHolder = treeRootHolder;
+    this.measureRepository = measureRepository;
+    this.metricRepository = metricRepository;
+  }
+
+  @Override
+  public void execute() {
+    new DepthTraversalTypeAwareVisitor(FILE, POST_ORDER) {
+      @Override
+      public void visitAny(Component component) {
+        List<Issue> issues = reportReader.readComponentIssues(component.getRef());
+        List<Issue> unresolvedIssues = from(issues)
+          .filter(UnresolvedIssue.INSTANCE)
+          .toList();
+        CountIssues countIssues = new CountIssues(unresolvedIssues);
+        addIssuesMeasures(component, unresolvedIssues);
+        addIssuesStatusMeasures(component, countIssues);
+        addIssuesSeverityMeasures(component, countIssues);
+        addFalsePositiveMeasures(component, issues);
+      }
+    }.visit(treeRootHolder.getRoot());
+  }
+
+  private void addIssuesMeasures(Component component, List<Issue> unresolvedIssues) {
+    addMeasure(component, VIOLATIONS_KEY, unresolvedIssues.size());
+    addNewMeasures(component, NEW_VIOLATIONS_KEY, unresolvedIssues);
+  }
+
+  private void addIssuesStatusMeasures(Component component, CountIssues countIssues) {
+    addMeasure(component, OPEN_ISSUES_KEY, countIssues.openIssues);
+    addMeasure(component, REOPENED_ISSUES_KEY, countIssues.reopenedIssues);
+    addMeasure(component, CONFIRMED_ISSUES_KEY, countIssues.confirmedIssues);
+  }
+
+  private void addIssuesSeverityMeasures(Component component, CountIssues countIssues) {
+    for (Map.Entry<String, String> entry : SEVERITY_METRIC_KEY_BY_SEVERITY.entrySet()) {
+      String severity = entry.getKey();
+      String metricKey = entry.getValue();
+      addMeasure(component, metricKey, countIssues.severityBag.count(severity));
+    }
+    for (Map.Entry<String, String> entry : NEW_SEVERITY_METRIC_KEY_BY_SEVERITY.entrySet()) {
+      String severity = entry.getKey();
+      String metricKey = entry.getValue();
+      addNewMeasures(component, metricKey, countIssues.issuesPerSeverity.get(severity));
+    }
+  }
+
+  private void addFalsePositiveMeasures(Component component, List<Issue> issues) {
+    addMeasure(component, FALSE_POSITIVE_ISSUES_KEY, from(issues).filter(FalsePositiveIssue.INSTANCE).size());
+  }
+
+  private void addNewMeasures(Component component, String metricKey, List<Issue> issues) {
+    if (periodsHolder.getPeriods().isEmpty()) {
+      return;
+    }
+    Metric metric = metricRepository.getByKey(metricKey);
+    Double[] periodValues = new Double[]{null, null, null, null, null};
+    for (Period period : periodsHolder.getPeriods()) {
+      Collection<Measure> childrenMeasures = getChildrenMeasures(component, metric);
+      double periodValue = sumIssuesOnPeriod(issues, period.getSnapshotDate()) + sumChildrenMeasuresOnPeriod(childrenMeasures, period.getIndex());
+      periodValues[period.getIndex() - 1] = periodValue;
+    }
+    measureRepository.add(component, metric, Measure.newMeasureBuilder()
+      .setVariations(new MeasureVariations(periodValues))
+      .createNoValue());
+  }
+
+  private void addMeasure(Component component, String metricKey, int value) {
+    Metric metric = metricRepository.getByKey(metricKey);
+    int totalValue = value + sumChildrenMeasures(getChildrenMeasures(component, metric));
+    measureRepository.add(component, metric, Measure.newMeasureBuilder().create(totalValue, null));
+  }
+
+  private Collection<Measure> getChildrenMeasures(Component component, final Metric metric) {
+    return from(component.getChildren()).transform(new ComponentChildrenMeasures(metric)).toList();
+  }
+
+  private static int sumChildrenMeasures(Collection<Measure> measures) {
+    SumMeasure sumMeasures = new SumMeasure();
+    from(measures).filter(sumMeasures).size();
+    return sumMeasures.getSum();
+  }
+
+  private static double sumChildrenMeasuresOnPeriod(Collection<Measure> measures, int periodIndex) {
+    SumVariationMeasure sumMeasures = new SumVariationMeasure(periodIndex);
+    from(measures).filter(sumMeasures).size();
+    return sumMeasures.getSum();
+  }
+
+  private static int sumIssuesOnPeriod(Collection<Issue> issues, long periodDate) {
+    // Add one second to not take into account issues created during current analysis
+    long datePlusOneSecond = periodDate + 1000L;
+    SumIssueAfterDate sumIssues = new SumIssueAfterDate(datePlusOneSecond);
+    from(issues).filter(sumIssues).toList();
+    return sumIssues.getSum();
+  }
+
+  private static class CountIssues {
+    int openIssues = 0;
+    int reopenedIssues = 0;
+    int confirmedIssues = 0;
+    Multiset<String> severityBag = HashMultiset.create();
+    ListMultimap<String, Issue> issuesPerSeverity = ArrayListMultimap.create();
+
+    public CountIssues(Iterable<Issue> issues) {
+      for (Issue issue : issues) {
+        countByStatus(issue.getStatus());
+        severityBag.add(issue.getSeverity().name());
+        issuesPerSeverity.put(issue.getSeverity().name(), issue);
+      }
+    }
+
+    private void countByStatus(String status) {
+      switch (status) {
+        case STATUS_OPEN:
+          openIssues++;
+          break;
+        case STATUS_REOPENED:
+          reopenedIssues++;
+          break;
+        case STATUS_CONFIRMED:
+          confirmedIssues++;
+          break;
+        default:
+          // Other statuses are ignored
+      }
+    }
+  }
+
+  private static class SumMeasure implements Predicate<Measure> {
+
+    private int sum = 0;
+
+    @Override
+    public boolean apply(@Nonnull Measure input) {
+      sum += input.getIntValue();
+      return true;
+    }
+
+    public int getSum() {
+      return sum;
+    }
+  }
+
+  private static class SumVariationMeasure implements Predicate<Measure> {
+
+    private final int periodIndex;
+    private double sum = 0d;
+
+    public SumVariationMeasure(int periodIndex) {
+      this.periodIndex = periodIndex;
+    }
+
+    @Override
+    public boolean apply(@Nonnull Measure input) {
+      if (input.hasVariations() && input.getVariations().hasVariation(periodIndex)) {
+        sum += input.getVariations().getVariation(periodIndex);
+      }
+      return true;
+    }
+
+    public double getSum() {
+      return sum;
+    }
+  }
+
+  private static class SumIssueAfterDate implements Predicate<Issue> {
+
+    private final long date;
+    private int sum = 0;
+
+    public SumIssueAfterDate(long date) {
+      this.date = date;
+    }
+
+    @Override
+    public boolean apply(@Nonnull Issue issue) {
+      if (isAfter(issue, date)) {
+        sum++;
+      }
+      return true;
+    }
+
+    public int getSum() {
+      return sum;
+    }
+
+    private static boolean isAfter(Issue issue, long date) {
+      // TODO should we truncate the date to the second as it was done in batch ?
+      return issue.getCreationDate() > date;
+    }
+  }
+
+  private enum FalsePositiveIssue implements Predicate<Issue> {
+    INSTANCE;
+
+    @Override
+    public boolean apply(@Nonnull Issue issue) {
+      return issue.hasResolution() && issue.getResolution().equals(RESOLUTION_FALSE_POSITIVE);
+    }
+  }
+
+  private enum UnresolvedIssue implements Predicate<Issue> {
+    INSTANCE;
+
+    @Override
+    public boolean apply(@Nonnull Issue issue) {
+      return !issue.hasResolution();
+    }
+  }
+
+  private class ComponentChildrenMeasures implements Function<Component, Measure> {
+    private final Metric metric;
+
+    public ComponentChildrenMeasures(Metric metric) {
+      this.metric = metric;
+    }
+
+    @Nullable
+    @Override
+    public Measure apply(@Nonnull Component input) {
+      Optional<Measure> childMeasure = measureRepository.getRawMeasure(input, metric);
+      if (childMeasure.isPresent()) {
+        return childMeasure.get();
+      }
+      return null;
+    }
+  }
+
+  @Override
+  public String getDescription() {
+    return "Compute measures on issues";
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/step/ComputeIssueMeasuresStepTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/step/ComputeIssueMeasuresStepTest.java
new file mode 100644 (file)
index 0000000..3e09534
--- /dev/null
@@ -0,0 +1,412 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.computation.step;
+
+import java.util.Arrays;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.utils.internal.Uuids;
+import org.sonar.batch.protocol.Constants;
+import org.sonar.batch.protocol.output.BatchReport;
+import org.sonar.batch.protocol.output.BatchReport.Issue;
+import org.sonar.core.rule.RuleDto;
+import org.sonar.server.computation.batch.BatchReportReaderRule;
+import org.sonar.server.computation.batch.TreeRootHolderRule;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.issue.RuleCache;
+import org.sonar.server.computation.measure.MeasureRepository;
+import org.sonar.server.computation.measure.MeasureRepositoryImpl;
+import org.sonar.server.computation.metric.Metric;
+import org.sonar.server.computation.metric.MetricImpl;
+import org.sonar.server.computation.metric.MetricRepository;
+import org.sonar.server.computation.period.Period;
+import org.sonar.server.computation.period.PeriodsHolderRule;
+import org.sonar.server.rule.RuleTesting;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
+import static org.sonar.api.issue.Issue.STATUS_CLOSED;
+import static org.sonar.api.issue.Issue.STATUS_CONFIRMED;
+import static org.sonar.api.issue.Issue.STATUS_OPEN;
+import static org.sonar.api.issue.Issue.STATUS_REOPENED;
+import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
+import static org.sonar.api.issue.internal.DefaultIssue.RESOLUTION_FIXED;
+import static org.sonar.api.issue.internal.DefaultIssue.RESOLUTION_REMOVED;
+import static org.sonar.api.measures.CoreMetrics.BLOCKER_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.CONFIRMED_ISSUES_KEY;
+import static org.sonar.api.measures.CoreMetrics.CRITICAL_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.FALSE_POSITIVE_ISSUES_KEY;
+import static org.sonar.api.measures.CoreMetrics.INFO_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.MAJOR_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.MINOR_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_BLOCKER_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_CRITICAL_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_INFO_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_MAJOR_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_MINOR_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_VIOLATIONS_KEY;
+import static org.sonar.api.measures.CoreMetrics.OPEN_ISSUES_KEY;
+import static org.sonar.api.measures.CoreMetrics.REOPENED_ISSUES_KEY;
+import static org.sonar.api.measures.CoreMetrics.VIOLATIONS_KEY;
+import static org.sonar.api.rule.Severity.BLOCKER;
+import static org.sonar.api.rule.Severity.CRITICAL;
+import static org.sonar.api.rule.Severity.INFO;
+import static org.sonar.api.rule.Severity.MAJOR;
+import static org.sonar.api.rule.Severity.MINOR;
+import static org.sonar.server.computation.component.DumbComponent.builder;
+import static org.sonar.server.computation.metric.Metric.MetricType.INT;
+
+public class ComputeIssueMeasuresStepTest {
+
+  static final Component FILE = builder(Component.Type.FILE, 2).build();
+  static final Component PROJECT = builder(Component.Type.PROJECT, 1).addChildren(FILE).build();
+
+  static final Metric ISSUES_METRIC = new MetricImpl(1, VIOLATIONS_KEY, VIOLATIONS_KEY, INT);
+  static final Metric OPEN_ISSUES_METRIC = new MetricImpl(2, OPEN_ISSUES_KEY, OPEN_ISSUES_KEY, INT);
+  static final Metric REOPENED_ISSUES_METRIC = new MetricImpl(3, REOPENED_ISSUES_KEY, REOPENED_ISSUES_KEY, INT);
+  static final Metric CONFIRMED_ISSUES_METRIC = new MetricImpl(4, CONFIRMED_ISSUES_KEY, CONFIRMED_ISSUES_KEY, INT);
+  static final Metric BLOCKER_ISSUES_METRIC = new MetricImpl(5, BLOCKER_VIOLATIONS_KEY, BLOCKER_VIOLATIONS_KEY, INT);
+  static final Metric CRITICAL_ISSUES_METRIC = new MetricImpl(6, CRITICAL_VIOLATIONS_KEY, CRITICAL_VIOLATIONS_KEY, INT);
+  static final Metric MAJOR_ISSUES_METRIC = new MetricImpl(7, MAJOR_VIOLATIONS_KEY, MAJOR_VIOLATIONS_KEY, INT);
+  static final Metric MINOR_ISSUES_METRIC = new MetricImpl(8, MINOR_VIOLATIONS_KEY, MINOR_VIOLATIONS_KEY, INT);
+  static final Metric INFO_ISSUES_METRIC = new MetricImpl(9, INFO_VIOLATIONS_KEY, INFO_VIOLATIONS_KEY, INT);
+  static final Metric NEW_ISSUES_METRIC = new MetricImpl(10, NEW_VIOLATIONS_KEY, NEW_VIOLATIONS_KEY, INT);
+  static final Metric NEW_BLOCKER_ISSUES_METRIC = new MetricImpl(11, NEW_BLOCKER_VIOLATIONS_KEY, NEW_BLOCKER_VIOLATIONS_KEY, INT);
+  static final Metric NEW_CRITICAL_ISSUES_METRIC = new MetricImpl(12, NEW_CRITICAL_VIOLATIONS_KEY, NEW_CRITICAL_VIOLATIONS_KEY, INT);
+  static final Metric NEW_MAJOR_ISSUES_METRIC = new MetricImpl(13, NEW_MAJOR_VIOLATIONS_KEY, NEW_MAJOR_VIOLATIONS_KEY, INT);
+  static final Metric NEW_MINOR_ISSUES_METRIC = new MetricImpl(14, NEW_MINOR_VIOLATIONS_KEY, NEW_MINOR_VIOLATIONS_KEY, INT);
+  static final Metric NEW_INFO_ISSUES_METRIC = new MetricImpl(15, NEW_INFO_VIOLATIONS_KEY, NEW_INFO_VIOLATIONS_KEY, INT);
+  static final Metric FALSE_POSITIVE_ISSUES_METRIC = new MetricImpl(16, FALSE_POSITIVE_ISSUES_KEY, FALSE_POSITIVE_ISSUES_KEY, INT);
+
+  static final RuleDto RULE_1 = RuleTesting.newDto(RuleKey.of("xoo", "x1")).setId(1);
+  static final RuleDto RULE_2 = RuleTesting.newDto(RuleKey.of("xoo", "x2")).setId(2);
+  static final RuleDto RULE_3 = RuleTesting.newDto(RuleKey.of("xoo", "x3")).setId(3);
+  static final RuleDto RULE_4 = RuleTesting.newDto(RuleKey.of("xoo", "x4")).setId(4);
+  static final RuleDto RULE_5 = RuleTesting.newDto(RuleKey.of("xoo", "x5")).setId(5);
+  static final RuleDto RULE_6 = RuleTesting.newDto(RuleKey.of("xoo", "x6")).setId(6);
+
+  @Rule
+  public BatchReportReaderRule reportReader = new BatchReportReaderRule();
+
+  @Rule
+  public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
+
+  @Rule
+  public PeriodsHolderRule periodsHolder = new PeriodsHolderRule();
+
+  MetricRepository metricRepository = mock(MetricRepository.class);
+  RuleCache ruleCache = mock(RuleCache.class);
+  MeasureRepository measureRepository;
+
+  ComputeIssueMeasuresStep sut;
+
+  @Before
+  public void setUp() throws Exception {
+    initMetrics();
+    measureRepository = new MeasureRepositoryImpl(null, reportReader, metricRepository, ruleCache);
+
+    sut = new ComputeIssueMeasuresStep(periodsHolder, reportReader, treeRootHolder, measureRepository, metricRepository);
+  }
+
+  @Test
+  public void compute_total_issues_measure() throws Exception {
+    treeRootHolder.setRoot(PROJECT);
+    periodsHolder.setPeriods();
+    addIssues(FILE.getRef(), createIssue(STATUS_OPEN, BLOCKER, RULE_1.getKey()));
+
+    sut.execute();
+
+    assertThat(measureRepository.getRawMeasure(PROJECT, ISSUES_METRIC).get().getIntValue()).isEqualTo(1);
+  }
+
+  @Test
+  public void compute_measures_on_all_levels() throws Exception {
+    Component file1 = builder(Component.Type.FILE, 5).build();
+    Component file2 = builder(Component.Type.FILE, 4).build();
+    Component file3 = builder(Component.Type.FILE, 3).build();
+    Component directory = builder(Component.Type.DIRECTORY, 2).addChildren(file1, file2, file3).build();
+    Component project = builder(Component.Type.PROJECT, 1).addChildren(directory).build();
+    treeRootHolder.setRoot(project);
+    periodsHolder.setPeriods();
+
+    addIssues(file1.getRef(), createIssue(STATUS_OPEN, BLOCKER, RULE_1.getKey()));
+    addIssues(file2.getRef(), createIssue(STATUS_REOPENED, CRITICAL, RULE_2.getKey()));
+
+    sut.execute();
+
+    assertThat(measureRepository.getRawMeasure(file1, ISSUES_METRIC).get().getIntValue()).isEqualTo(1);
+    assertThat(measureRepository.getRawMeasure(file2, ISSUES_METRIC).get().getIntValue()).isEqualTo(1);
+    assertThat(measureRepository.getRawMeasure(directory, ISSUES_METRIC).get().getIntValue()).isEqualTo(2);
+    assertThat(measureRepository.getRawMeasure(project, ISSUES_METRIC).get().getIntValue()).isEqualTo(2);
+  }
+
+  @Test
+  public void compute_measures_on_issue_statuses() throws Exception {
+    treeRootHolder.setRoot(PROJECT);
+    periodsHolder.setPeriods();
+    addIssues(FILE.getRef(),
+      createIssue(STATUS_OPEN, BLOCKER, RULE_1.getKey()),
+      createIssue(STATUS_REOPENED, BLOCKER, RULE_2.getKey()),
+      createIssue(STATUS_CONFIRMED, BLOCKER, RULE_3.getKey()),
+      createIssue(STATUS_CONFIRMED, BLOCKER, RULE_4.getKey()));
+
+    sut.execute();
+
+    assertThat(measureRepository.getRawMeasure(PROJECT, OPEN_ISSUES_METRIC).get().getIntValue()).isEqualTo(1);
+    assertThat(measureRepository.getRawMeasure(PROJECT, REOPENED_ISSUES_METRIC).get().getIntValue()).isEqualTo(1);
+    assertThat(measureRepository.getRawMeasure(PROJECT, CONFIRMED_ISSUES_METRIC).get().getIntValue()).isEqualTo(2);
+  }
+
+  @Test
+  public void compute_measures_on_issue_severities() throws Exception {
+    treeRootHolder.setRoot(PROJECT);
+    periodsHolder.setPeriods();
+    addIssues(FILE.getRef(),
+      createIssue(STATUS_OPEN, BLOCKER, RULE_1.getKey()),
+      createIssue(STATUS_OPEN, CRITICAL, RULE_2.getKey()),
+      createIssue(STATUS_OPEN, MAJOR, RULE_3.getKey()),
+      createIssue(STATUS_OPEN, MINOR, RULE_4.getKey()),
+      createIssue(STATUS_OPEN, INFO, RULE_5.getKey()),
+      createIssue(STATUS_OPEN, INFO, RULE_6.getKey()));
+
+    sut.execute();
+
+    assertThat(measureRepository.getRawMeasure(PROJECT, BLOCKER_ISSUES_METRIC).get().getIntValue()).isEqualTo(1);
+    assertThat(measureRepository.getRawMeasure(PROJECT, CRITICAL_ISSUES_METRIC).get().getIntValue()).isEqualTo(1);
+    assertThat(measureRepository.getRawMeasure(PROJECT, MAJOR_ISSUES_METRIC).get().getIntValue()).isEqualTo(1);
+    assertThat(measureRepository.getRawMeasure(PROJECT, MINOR_ISSUES_METRIC).get().getIntValue()).isEqualTo(1);
+    assertThat(measureRepository.getRawMeasure(PROJECT, INFO_ISSUES_METRIC).get().getIntValue()).isEqualTo(2);
+  }
+
+  @Test
+  public void compute_measures_on_false_positive_issue() throws Exception {
+    treeRootHolder.setRoot(PROJECT);
+    periodsHolder.setPeriods();
+    addIssues(FILE.getRef(),
+      createIssue(STATUS_OPEN, BLOCKER, RULE_1.getKey()),
+      createIssue(STATUS_CLOSED, BLOCKER, RESOLUTION_FALSE_POSITIVE, RULE_2.getKey()),
+      createIssue(STATUS_RESOLVED, BLOCKER, RESOLUTION_FIXED, RULE_3.getKey()),
+      createIssue(STATUS_RESOLVED, BLOCKER, RESOLUTION_REMOVED, RULE_4.getKey()));
+
+    sut.execute();
+
+    assertThat(measureRepository.getRawMeasure(PROJECT, FALSE_POSITIVE_ISSUES_METRIC).get().getIntValue()).isEqualTo(1);
+  }
+
+  @Test
+  public void compute_measures_on_new_issue() throws Exception {
+    treeRootHolder.setRoot(PROJECT);
+    addIssues(FILE.getRef(),
+      // issue created before the period 3
+      createIssue(STATUS_CONFIRMED, BLOCKER, null, RULE_1.getKey(), 1388552400000L),
+      // issue created after period 3 but before current analysis
+      createIssue(STATUS_OPEN, BLOCKER, null, RULE_1.getKey(), 1433131200000L));
+    periodsHolder.setPeriods(newPeriod(3, 1420088400000L));
+
+    sut.execute();
+
+    // Only 1 new issues for period 3
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_ISSUES_METRIC).get().getVariations().getVariation3()).isEqualTo(1);
+
+    // No variation on other periods
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_ISSUES_METRIC).get().getVariations().hasVariation1()).isFalse();
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_ISSUES_METRIC).get().getVariations().hasVariation2()).isFalse();
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_ISSUES_METRIC).get().getVariations().hasVariation4()).isFalse();
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_ISSUES_METRIC).get().getVariations().hasVariation5()).isFalse();
+  }
+
+  @Test
+  public void do_not_take_into_account_issue_from_current_analysis_when_computing_measures_on_new_issue() throws Exception {
+    treeRootHolder.setRoot(PROJECT);
+    addIssues(FILE.getRef(),
+      // issue created during current analysis -> should not be taking into account
+      createIssue(STATUS_OPEN, BLOCKER, null, RULE_1.getKey(), 1420088400000L));
+    periodsHolder.setPeriods(newPeriod(1, 1420088400000L));
+
+    sut.execute();
+
+    // No new issues
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_ISSUES_METRIC).get().getVariations().getVariation1()).isEqualTo(0);
+  }
+
+  @Test
+  public void compute_measures_on_new_issue_on_every_variations() throws Exception {
+    treeRootHolder.setRoot(PROJECT);
+    addIssues(FILE.getRef(),
+      // issue created the 2014-01-01, before all periods
+      createIssue(STATUS_CONFIRMED, BLOCKER, null, RULE_1.getKey(), 1388552400000L),
+      // issue created the 2015-01-15, before period 2
+      createIssue(STATUS_CONFIRMED, BLOCKER, null, RULE_1.getKey(), 1421298000000L),
+      // issue created the 2015-02-15, before period 3
+      createIssue(STATUS_CONFIRMED, BLOCKER, null, RULE_1.getKey(), 1423976400000L),
+      // issue created the 2015-03-15, before period 4
+      createIssue(STATUS_CONFIRMED, BLOCKER, null, RULE_1.getKey(), 1426392000000L),
+      // issue created the 2015-04-15, before period 5
+      createIssue(STATUS_CONFIRMED, BLOCKER, null, RULE_1.getKey(), 1429070400000L),
+      // issue created the 2015-06-01 -> Should not been taken into account by any period
+      createIssue(STATUS_OPEN, BLOCKER, null, RULE_1.getKey(), 1433131200000L));
+    periodsHolder.setPeriods(
+      // 2015-01-01
+      newPeriod(1, 1420088400000L),
+      // 2015-02-01
+      newPeriod(2, 1422766800000L),
+      // 2015-03-01
+      newPeriod(3, 1425186000000L),
+      // 2015-04-01
+      newPeriod(4, 1427860800000L),
+      // 2015-05-01
+      newPeriod(5, 1430452800000L));
+
+    sut.execute();
+
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_ISSUES_METRIC).get().getVariations().getVariation1()).isEqualTo(5);
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_ISSUES_METRIC).get().getVariations().getVariation2()).isEqualTo(4);
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_ISSUES_METRIC).get().getVariations().getVariation3()).isEqualTo(3);
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_ISSUES_METRIC).get().getVariations().getVariation4()).isEqualTo(2);
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_ISSUES_METRIC).get().getVariations().getVariation5()).isEqualTo(1);
+  }
+
+  @Test
+  public void compute_measures_on_new_issue_severities() throws Exception {
+    treeRootHolder.setRoot(PROJECT);
+    addIssues(FILE.getRef(),
+      // issue created before the period 1
+      createIssue(STATUS_CONFIRMED, BLOCKER, null, RULE_1.getKey(), 1388552400000L),
+      // issues created after period 1 but before current analysis
+      createIssue(STATUS_OPEN, BLOCKER, null, RULE_1.getKey(), 1433131200000L),
+      createIssue(STATUS_OPEN, BLOCKER, null, RULE_2.getKey(), 1433131200000L),
+      createIssue(STATUS_OPEN, CRITICAL, null, RULE_1.getKey(), 1433131200000L),
+      createIssue(STATUS_OPEN, MAJOR, null, RULE_1.getKey(), 1433131200000L),
+      createIssue(STATUS_OPEN, MINOR, null, RULE_1.getKey(), 1433131200000L),
+      createIssue(STATUS_OPEN, INFO, null, RULE_1.getKey(), 1433131200000L));
+    periodsHolder.setPeriods(newPeriod(1, 1420088400000L));
+
+    sut.execute();
+
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_BLOCKER_ISSUES_METRIC).get().getVariations().getVariation1()).isEqualTo(2);
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_CRITICAL_ISSUES_METRIC).get().getVariations().getVariation1()).isEqualTo(1);
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_MAJOR_ISSUES_METRIC).get().getVariations().getVariation1()).isEqualTo(1);
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_MINOR_ISSUES_METRIC).get().getVariations().getVariation1()).isEqualTo(1);
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_INFO_ISSUES_METRIC).get().getVariations().getVariation1()).isEqualTo(1);
+  }
+
+  @Test
+  public void compute_no_new_measures_when_no_period() throws Exception {
+    treeRootHolder.setRoot(PROJECT);
+    periodsHolder.setPeriods();
+    addIssues(FILE.getRef(),
+      createIssue(STATUS_CONFIRMED, BLOCKER, null, RULE_1.getKey(), 1388552400000L));
+
+    sut.execute();
+
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_ISSUES_METRIC).isPresent()).isFalse();
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_BLOCKER_ISSUES_METRIC).isPresent()).isFalse();
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_CRITICAL_ISSUES_METRIC).isPresent()).isFalse();
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_MAJOR_ISSUES_METRIC).isPresent()).isFalse();
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_MINOR_ISSUES_METRIC).isPresent()).isFalse();
+    assertThat(measureRepository.getRawMeasure(PROJECT, NEW_INFO_ISSUES_METRIC).isPresent()).isFalse();
+  }
+
+  @Test
+  public void compute_measures_having_zero_value_if_no_issue() throws Exception {
+    treeRootHolder.setRoot(PROJECT);
+    periodsHolder.setPeriods();
+
+    sut.execute();
+
+    assertThat(measureRepository.getRawMeasure(PROJECT, ISSUES_METRIC).get().getIntValue()).isEqualTo(0);
+    assertThat(measureRepository.getRawMeasure(PROJECT, OPEN_ISSUES_METRIC).get().getIntValue()).isEqualTo(0);
+    assertThat(measureRepository.getRawMeasure(PROJECT, REOPENED_ISSUES_METRIC).get().getIntValue()).isEqualTo(0);
+    assertThat(measureRepository.getRawMeasure(PROJECT, BLOCKER_ISSUES_METRIC).get().getIntValue()).isEqualTo(0);
+    assertThat(measureRepository.getRawMeasure(PROJECT, CRITICAL_ISSUES_METRIC).get().getIntValue()).isEqualTo(0);
+    assertThat(measureRepository.getRawMeasure(PROJECT, MAJOR_ISSUES_METRIC).get().getIntValue()).isEqualTo(0);
+    assertThat(measureRepository.getRawMeasure(PROJECT, MINOR_ISSUES_METRIC).get().getIntValue()).isEqualTo(0);
+    assertThat(measureRepository.getRawMeasure(PROJECT, INFO_ISSUES_METRIC).get().getIntValue()).isEqualTo(0);
+  }
+
+  @Test
+  public void ignore_resolved_issues() throws Exception {
+    treeRootHolder.setRoot(PROJECT);
+    periodsHolder.setPeriods();
+    addIssues(FILE.getRef(),
+      createIssue(STATUS_CLOSED, BLOCKER, RESOLUTION_FALSE_POSITIVE, RULE_1.getKey()),
+      createIssue(STATUS_RESOLVED, BLOCKER, RESOLUTION_FIXED, RULE_2.getKey()),
+      createIssue(STATUS_RESOLVED, BLOCKER, RESOLUTION_REMOVED, RULE_3.getKey()));
+
+    sut.execute();
+
+    assertThat(measureRepository.getRawMeasure(PROJECT, ISSUES_METRIC).get().getIntValue()).isEqualTo(0);
+  }
+
+  private void addIssues(int componentRef, Issue... issues) {
+    reportReader.putIssues(componentRef, Arrays.asList(issues));
+  }
+
+  private static Issue createIssue(String status, String severity, RuleKey ruleKey) {
+    return createIssue(status, severity, null, ruleKey, 1000L);
+  }
+
+  private static Issue createIssue(String status, String severity, @Nullable String resolution, RuleKey ruleKey) {
+    return createIssue(status, severity, resolution, ruleKey, 1000L);
+  }
+
+  private static Issue createIssue(String status, String severity, @Nullable String resolution, RuleKey ruleKey, long creationDate) {
+    BatchReport.Issue.Builder issueBuilder = Issue.newBuilder()
+      .setUuid(Uuids.create())
+      .setStatus(status)
+      .setRuleKey(ruleKey.rule())
+      .setRuleRepository(ruleKey.repository())
+      .setSeverity(Constants.Severity.valueOf(severity))
+      .setCreationDate(creationDate);
+    if (resolution != null) {
+      issueBuilder.setResolution(resolution);
+    }
+    return issueBuilder.build();
+  }
+
+  private static Period newPeriod(int index, long date) {
+    return new Period(index, "mode", null, date, 42l);
+  }
+
+  private void initMetrics() {
+    when(metricRepository.getByKey(ISSUES_METRIC.getKey())).thenReturn(ISSUES_METRIC);
+    when(metricRepository.getByKey(OPEN_ISSUES_METRIC.getKey())).thenReturn(OPEN_ISSUES_METRIC);
+    when(metricRepository.getByKey(REOPENED_ISSUES_METRIC.getKey())).thenReturn(REOPENED_ISSUES_METRIC);
+    when(metricRepository.getByKey(CONFIRMED_ISSUES_METRIC.getKey())).thenReturn(CONFIRMED_ISSUES_METRIC);
+    when(metricRepository.getByKey(BLOCKER_ISSUES_METRIC.getKey())).thenReturn(BLOCKER_ISSUES_METRIC);
+    when(metricRepository.getByKey(CRITICAL_ISSUES_METRIC.getKey())).thenReturn(CRITICAL_ISSUES_METRIC);
+    when(metricRepository.getByKey(MAJOR_ISSUES_METRIC.getKey())).thenReturn(MAJOR_ISSUES_METRIC);
+    when(metricRepository.getByKey(MINOR_ISSUES_METRIC.getKey())).thenReturn(MINOR_ISSUES_METRIC);
+    when(metricRepository.getByKey(INFO_ISSUES_METRIC.getKey())).thenReturn(INFO_ISSUES_METRIC);
+    when(metricRepository.getByKey(NEW_ISSUES_METRIC.getKey())).thenReturn(NEW_ISSUES_METRIC);
+    when(metricRepository.getByKey(NEW_BLOCKER_ISSUES_METRIC.getKey())).thenReturn(NEW_BLOCKER_ISSUES_METRIC);
+    when(metricRepository.getByKey(NEW_CRITICAL_ISSUES_METRIC.getKey())).thenReturn(NEW_CRITICAL_ISSUES_METRIC);
+    when(metricRepository.getByKey(NEW_MAJOR_ISSUES_METRIC.getKey())).thenReturn(NEW_MAJOR_ISSUES_METRIC);
+    when(metricRepository.getByKey(NEW_MINOR_ISSUES_METRIC.getKey())).thenReturn(NEW_MINOR_ISSUES_METRIC);
+    when(metricRepository.getByKey(NEW_INFO_ISSUES_METRIC.getKey())).thenReturn(NEW_INFO_ISSUES_METRIC);
+    when(metricRepository.getByKey(FALSE_POSITIVE_ISSUES_METRIC.getKey())).thenReturn(FALSE_POSITIVE_ISSUES_METRIC);
+  }
+}