From 52cc995fc8b9af6b66b2f313d9048481345fc6f1 Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Fri, 19 Jun 2015 14:49:52 +0200 Subject: [PATCH] SONAR-6646 Compute measures on issues --- .../computation/step/ComputationSteps.java | 4 +- .../step/ComputeIssueMeasuresStep.java | 341 +++++++++++++++ .../step/ComputeIssueMeasuresStepTest.java | 412 ++++++++++++++++++ 3 files changed, 756 insertions(+), 1 deletion(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputeIssueMeasuresStep.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/computation/step/ComputeIssueMeasuresStepTest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java b/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java index 043b0ddf8be..dcbb8a2b200 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java @@ -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 index 00000000000..1fd30585f08 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputeIssueMeasuresStep.java @@ -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 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 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 issues = reportReader.readComponentIssues(component.getRef()); + List 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 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 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 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 issues) { + addMeasure(component, FALSE_POSITIVE_ISSUES_KEY, from(issues).filter(FalsePositiveIssue.INSTANCE).size()); + } + + private void addNewMeasures(Component component, String metricKey, List 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 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 getChildrenMeasures(Component component, final Metric metric) { + return from(component.getChildren()).transform(new ComponentChildrenMeasures(metric)).toList(); + } + + private static int sumChildrenMeasures(Collection measures) { + SumMeasure sumMeasures = new SumMeasure(); + from(measures).filter(sumMeasures).size(); + return sumMeasures.getSum(); + } + + private static double sumChildrenMeasuresOnPeriod(Collection measures, int periodIndex) { + SumVariationMeasure sumMeasures = new SumVariationMeasure(periodIndex); + from(measures).filter(sumMeasures).size(); + return sumMeasures.getSum(); + } + + private static int sumIssuesOnPeriod(Collection 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 severityBag = HashMultiset.create(); + ListMultimap issuesPerSeverity = ArrayListMultimap.create(); + + public CountIssues(Iterable 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 { + + 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 { + + 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 { + + 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 { + INSTANCE; + + @Override + public boolean apply(@Nonnull Issue issue) { + return issue.hasResolution() && issue.getResolution().equals(RESOLUTION_FALSE_POSITIVE); + } + } + + private enum UnresolvedIssue implements Predicate { + INSTANCE; + + @Override + public boolean apply(@Nonnull Issue issue) { + return !issue.hasResolution(); + } + } + + private class ComponentChildrenMeasures implements Function { + private final Metric metric; + + public ComponentChildrenMeasures(Metric metric) { + this.metric = metric; + } + + @Nullable + @Override + public Measure apply(@Nonnull Component input) { + Optional 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 index 00000000000..3e09534b789 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/step/ComputeIssueMeasuresStepTest.java @@ -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); + } +} -- 2.39.5