extensions.add(ViolationPersisterDecorator.class);
extensions.add(NewViolationsDecorator.class);
extensions.add(TimeMachineConfigurationPersister.class);
+ extensions.add(NewCoverageDecorator.class);
return extensions;
}
--- /dev/null
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package org.sonar.plugins.core.timemachine;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.apache.commons.lang.NumberUtils;
+import org.apache.commons.lang.ObjectUtils;
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.DependedUpon;
+import org.sonar.api.batch.DependsUpon;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.Scopes;
+import org.sonar.api.utils.KeyValueFormat;
+import org.sonar.batch.components.PastSnapshot;
+import org.sonar.batch.components.TimeMachineConfiguration;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @since 2.7
+ */
+public final class NewCoverageDecorator implements Decorator {
+
+ private List<PeriodStruct> structs;
+
+ public NewCoverageDecorator(TimeMachineConfiguration timeMachineConfiguration) {
+ structs = Lists.newArrayList();
+ for (PastSnapshot pastSnapshot : timeMachineConfiguration.getProjectPastSnapshots()) {
+ structs.add(new PeriodStruct(pastSnapshot));
+ }
+ }
+
+ NewCoverageDecorator(List<PeriodStruct> structs) {
+ this.structs = structs;
+ }
+
+
+ public boolean shouldExecuteOnProject(Project project) {
+ return project.isLatestAnalysis() && !structs.isEmpty();
+ }
+
+ private boolean shouldDecorate(Resource resource) {
+ return isFile(resource);
+ }
+
+ @DependsUpon
+ public List<Metric> dependsOnMetrics() {
+ return Arrays.asList(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, CoreMetrics.COVERAGE_LINE_HITS_DATA,
+ CoreMetrics.CONDITIONS_BY_LINE, CoreMetrics.COVERED_CONDITIONS_BY_LINE);
+ }
+
+ @DependedUpon
+ public List<Metric> generatesNewCoverageMetrics() {
+ return Arrays.asList(CoreMetrics.NEW_LINES_TO_COVER, CoreMetrics.NEW_UNCOVERED_LINES,
+ CoreMetrics.NEW_CONDITIONS_TO_COVER, CoreMetrics.NEW_UNCOVERED_CONDITIONS);
+ }
+
+ public void decorate(Resource resource, DecoratorContext context) {
+ if (shouldDecorate(resource)) {
+ doDecorate(context);
+ }
+ }
+
+ void doDecorate(DecoratorContext context) {
+ if (parse(context)) {
+ compute(context);
+ }
+ }
+
+ private boolean parse(DecoratorContext context) {
+ Measure lastCommits = context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE);
+ Measure hitsByLineMeasure = context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA);
+
+ if (lastCommits != null && lastCommits.hasData() && hitsByLineMeasure != null && hitsByLineMeasure.hasData()) {
+ Map<Integer, Date> datesByLine = KeyValueFormat.parseIntDateTime(lastCommits.getData());
+ Map<Integer, Integer> hitsByLine = parseCountByLine(hitsByLineMeasure);
+ Map<Integer, Integer> conditionsByLine = parseCountByLine(context.getMeasure(CoreMetrics.CONDITIONS_BY_LINE));
+ Map<Integer, Integer> coveredConditionsByLine = parseCountByLine(context.getMeasure(CoreMetrics.COVERED_CONDITIONS_BY_LINE));
+
+ reset();
+
+ for (Map.Entry<Integer, Integer> entry : hitsByLine.entrySet()) {
+ int lineId = entry.getKey();
+ int hits = entry.getValue();
+ int conditions = (Integer)ObjectUtils.defaultIfNull(conditionsByLine.get(lineId), 0);
+ int coveredConditions = (Integer)ObjectUtils.defaultIfNull(coveredConditionsByLine.get(lineId), 0);
+ Date date = datesByLine.get(lineId);
+ for (PeriodStruct struct : structs) {
+ struct.analyze(date, hits, conditions, coveredConditions);
+ }
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ private void reset() {
+ for (PeriodStruct struct : structs) {
+ struct.reset();
+ }
+ }
+
+ private void compute(DecoratorContext context) {
+ Measure newLines = new Measure(CoreMetrics.NEW_LINES_TO_COVER);
+ Measure newUncoveredLines = new Measure(CoreMetrics.NEW_UNCOVERED_LINES);
+ Measure newConditions = new Measure(CoreMetrics.NEW_CONDITIONS_TO_COVER);
+ Measure newUncoveredConditions = new Measure(CoreMetrics.NEW_UNCOVERED_CONDITIONS);
+
+ for (PeriodStruct struct : structs) {
+ newLines.setVariation(struct.index, (double)struct.newLines);
+ newUncoveredLines.setVariation(struct.index, (double) (struct.newLines - struct.newCoveredLines));
+ newConditions.setVariation(struct.index, (double)struct.newConditions);
+ newUncoveredConditions.setVariation(struct.index, (double)struct.newConditions-struct.newCoveredConditions);
+ }
+
+ context.saveMeasure(newLines);
+ context.saveMeasure(newUncoveredLines);
+ context.saveMeasure(newConditions);
+ context.saveMeasure(newUncoveredConditions);
+ }
+
+
+ private Map<Integer, Integer> parseCountByLine(Measure measure) {
+ if (measure != null && measure.hasData()) {
+ return KeyValueFormat.parseIntInt(measure.getData());
+ }
+ return Maps.newHashMap();
+ }
+
+
+ private boolean isFile(Resource resource) {
+ return Scopes.isFile(resource) && !Qualifiers.UNIT_TEST_FILE.equals(resource.getQualifier());
+ }
+
+ public static final class PeriodStruct {
+ int index;
+ Date date;
+ int newLines = 0, newCoveredLines = 0, newConditions = 0, newCoveredConditions = 0;
+
+ PeriodStruct(PastSnapshot pastSnapshot) {
+ this.index = pastSnapshot.getIndex();
+ this.date = pastSnapshot.getDate();
+ }
+
+ PeriodStruct(int index, Date date) {
+ this.index = index;
+ this.date = date;
+ }
+
+ void reset() {
+ newLines = 0;
+ newCoveredLines = 0;
+ newConditions = 0;
+ newCoveredConditions = 0;
+ }
+
+ void analyze(Date lineDate, int hits, int conditions, int coveredConditions) {
+ if (lineDate == null) {
+ //TODO warning
+
+ } else if (lineDate.after(date)) {
+ // TODO experiment if date comparison is faster or not
+ addLine(hits > 0);
+ addConditions(conditions, coveredConditions);
+ }
+ }
+
+ void addLine(boolean covered) {
+ newLines += 1;
+ if (covered) {
+ newCoveredLines += 1;
+ }
+ }
+
+ void addConditions(int count, int countCovered) {
+ newConditions += count;
+ if (count > 0) {
+ newCoveredConditions += countCovered;
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package org.sonar.plugins.core.timemachine;
+
+import org.apache.commons.lang.NumberUtils;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.junit.Test;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.utils.DateUtils;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+import static org.mockito.Mockito.*;
+
+public class NewCoverageDecoratorTest {
+
+ @Test
+ public void shouldDoNothingIfNoScmData() throws ParseException {
+ DecoratorContext context = mock(DecoratorContext.class);
+ when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA))
+ .thenReturn(new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "1=10"));
+
+ NewCoverageDecorator decorator = newDecorator();
+ decorator.doDecorate(context);
+ verify(context, never()).saveMeasure((Measure) anyObject());
+ }
+
+ @Test
+ public void shouldDoNothingIfNoCoverageData() throws ParseException {
+ DecoratorContext context = mock(DecoratorContext.class);
+ when(context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE))
+ .thenReturn(new Measure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, "10=2008-05-18T00:00:00+0000"));
+
+ NewCoverageDecorator decorator = newDecorator();
+ decorator.doDecorate(context);
+
+ verify(context, never()).saveMeasure((Measure) anyObject());
+ }
+
+ @Test
+ public void shouldGetNewLines() throws ParseException {
+ DecoratorContext context = mock(DecoratorContext.class);
+ when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA)).thenReturn(
+ new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "10=2;11=3"));
+ when(context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE)).thenReturn(
+ new Measure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, "10=2007-01-15T00:00:00+0000;11=2011-01-01T00:00:00+0000"));
+
+ NewCoverageDecorator decorator = newDecorator();
+ decorator.doDecorate(context);
+
+ // line 11 has been updated after date1 (2009-12-25). This line is covered.
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 1, 1.0)));
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 1, 0.0)));
+
+ // no line have been updated after date3 (2011-02-18)
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 3, 0.0)));
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 3, 0.0)));
+
+ // no data on other periods
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 2, null)));
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 4, null)));
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 5, null)));
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 2, null)));
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 4, null)));
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 5, null)));
+ }
+
+ @Test
+ public void shouldGetNewConditions() throws ParseException {
+ DecoratorContext context = mock(DecoratorContext.class);
+ when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA)).thenReturn(
+ new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "10=2;11=3"));
+ when(context.getMeasure(CoreMetrics.CONDITIONS_BY_LINE)).thenReturn(
+ new Measure(CoreMetrics.CONDITIONS_BY_LINE, "11=4"));
+ when(context.getMeasure(CoreMetrics.COVERED_CONDITIONS_BY_LINE)).thenReturn(
+ new Measure(CoreMetrics.COVERED_CONDITIONS_BY_LINE, "11=1"));
+ when(context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE)).thenReturn(
+ new Measure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, "10=2007-01-15T00:00:00+0000;11=2011-01-01T00:00:00+0000"));
+
+ NewCoverageDecorator decorator = newDecorator();
+ decorator.doDecorate(context);
+
+ // line 11 has been updated after date1 (2009-12-25). This line has 1 covered condition amongst 4
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 1, 4.0)));
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 1, 3.0)));
+
+ // no line have been updated after date3 (2011-02-18)
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 3, 0.0)));
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 3, 0.0)));
+
+ // no data on other periods
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 2, null)));
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 4, null)));
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 5, null)));
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 2, null)));
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 4, null)));
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 5, null)));
+ }
+
+ @Test
+ public void shouldNotGetNewConditionsWhenNewLineHasNoConditions() throws ParseException {
+ DecoratorContext context = mock(DecoratorContext.class);
+ when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA)).thenReturn(
+ new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "10=2;11=3"));
+ when(context.getMeasure(CoreMetrics.CONDITIONS_BY_LINE)).thenReturn(
+ new Measure(CoreMetrics.CONDITIONS_BY_LINE, "10=1"));
+ when(context.getMeasure(CoreMetrics.COVERED_CONDITIONS_BY_LINE)).thenReturn(
+ new Measure(CoreMetrics.COVERED_CONDITIONS_BY_LINE, "10=1"));
+ when(context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE)).thenReturn(
+ new Measure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, "10=2007-01-15T00:00:00+0000;11=2011-01-01T00:00:00+0000"));
+
+ NewCoverageDecorator decorator = newDecorator();
+ decorator.doDecorate(context);
+
+ // line 11 has been updated after date1 (2009-12-25) but it has no conditions
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 1, 0.0)));
+ verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 1, 0.0)));
+ }
+
+
+ static class VariationMatcher extends BaseMatcher<Measure> {
+ Metric metric;
+ int index;
+ Double variation;
+
+ VariationMatcher(Metric metric, int index, Double variation) {
+ this.metric = metric;
+ this.index = index;
+ this.variation = variation;
+ }
+
+ public boolean matches(Object o) {
+ Measure m = (Measure)o;
+ if (m.getMetric().equals(metric)) {
+ if ((variation==null && m.getVariation(index)==null) ||
+ (variation!=null && variation.equals(m.getVariation(index)))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void describeTo(Description description) {
+
+ }
+ }
+
+ private NewCoverageDecorator newDecorator() throws ParseException {
+ List<NewCoverageDecorator.PeriodStruct> structs = Arrays.asList(
+ new NewCoverageDecorator.PeriodStruct(1, newDate("2009-12-25")),
+ new NewCoverageDecorator.PeriodStruct(3, newDate("2011-02-18")));
+ return new NewCoverageDecorator(structs);
+ }
+
+ private Date newDate(String s) throws ParseException {
+ return new SimpleDateFormat(DateUtils.DATE_FORMAT).parse(s);
+ }
+}
public static final Metric LINES_TO_COVER = new Metric(LINES_TO_COVER_KEY, "Lines to cover", "Lines to cover", Metric.ValueType.INT,
Metric.DIRECTION_BETTER, false, DOMAIN_TESTS).setFormula(new SumChildValuesFormula(false)).setHidden(true);
+ public static final String NEW_LINES_TO_COVER_KEY = "new_lines_to_cover";
+ public static final Metric NEW_LINES_TO_COVER = new Metric.Builder(NEW_LINES_TO_COVER_KEY, Metric.ValueType.INT)
+ .setName("New lines to cover")
+ .setDescription("New lines to cover")
+ .setDirection(Metric.DIRECTION_BETTER)
+ .setDomain(DOMAIN_TESTS)
+ .setFormula(new SumChildValuesFormula(false))
+ .setHidden(true)
+ .create();
public static final String UNCOVERED_LINES_KEY = "uncovered_lines";
public static final Metric UNCOVERED_LINES = new Metric.Builder(UNCOVERED_LINES_KEY, Metric.ValueType.INT)
public static final Metric CONDITIONS_TO_COVER = new Metric.Builder(CONDITIONS_TO_COVER_KEY, Metric.ValueType.INT)
.setName("Conditions to cover")
.setDescription("Conditions to cover")
- .setDirection(Metric.DIRECTION_BETTER)
+ .setDomain(DOMAIN_TESTS)
+ .setFormula(new SumChildValuesFormula(false))
+ .setHidden(true)
+ .create();
+
+ public static final String NEW_CONDITIONS_TO_COVER_KEY = "new_conditions_to_cover";
+ public static final Metric NEW_CONDITIONS_TO_COVER = new Metric.Builder(NEW_CONDITIONS_TO_COVER_KEY, Metric.ValueType.INT)
+ .setName("New conditions to cover")
+ .setDescription("New conditions to cover")
.setDomain(DOMAIN_TESTS)
.setFormula(new SumChildValuesFormula(false))
.setHidden(true)
Metric.ValueType.INT, Metric.DIRECTION_BETTER, false, DOMAIN_DESIGN).setHidden(true);
-
// Alerts
public static final String ALERT_STATUS_KEY = "alert_status";
public static final Metric ALERT_STATUS = new Metric.Builder(ALERT_STATUS_KEY, Metric.ValueType.LEVEL)
.setQualitative(true)
.setDomain(DOMAIN_GENERAL)
.create();
-
+
/* quality profile */
public static final String PROFILE_KEY = "profile";
--- /dev/null
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package org.sonar.api.utils;
+
+/**
+ * @since 2.7
+ */
+public interface DateUtils {
+ String DATE_FORMAT = "yyyy-MM-dd";
+ String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
+}
}
public static class DateConverter extends Converter<Date> {
- private DateFormat dateFormat;
+ private SimpleDateFormat dateFormat;
public DateConverter() {
- this("yyyy-MM-dd");
+ this(DateUtils.DATE_FORMAT);
}
DateConverter(String format) {
try {
return StringUtils.isBlank(s) ? null : dateFormat.parse(s);
} catch (ParseException e) {
- throw new SonarException("Not a date: " + s, e);
+ throw new SonarException("Not a date with format: " + dateFormat.toPattern(), e);
}
}
}
public static class DateTimeConverter extends DateConverter {
public DateTimeConverter() {
- super("yyyy-MM-dd'T'HH:mm:ssZ");
+ super(DateUtils.DATETIME_FORMAT);
}
}
return parse(data, IntegerConverter.INSTANCE, new DateConverter());
}
+ /**
+ * @since 2.7
+ */
+ public static Map<Integer, Integer> parseIntInt(String data) {
+ return parse(data, IntegerConverter.INSTANCE, IntegerConverter.INSTANCE);
+ }
+
/**
* @since 2.7
*/