]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5216 Generate profile events for multi language projects
authorJulien HENRY <julien.henry@sonarsource.com>
Mon, 19 May 2014 08:50:08 +0000 (10:50 +0200)
committerJulien HENRY <julien.henry@sonarsource.com>
Mon, 19 May 2014 09:31:11 +0000 (11:31 +0200)
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileEventsSensor.java [deleted file]
plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/description.html.erb
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileEventsSensorTest.java [deleted file]
sonar-batch/src/main/java/org/sonar/batch/rule/QProfileDecorator.java
sonar-batch/src/main/java/org/sonar/batch/rule/QProfileEventsDecorator.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/rule/UsedQProfiles.java
sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java
sonar-batch/src/test/java/org/sonar/batch/rule/QProfileEventsDecoratorTest.java [new file with mode: 0644]

index 6a7499e2332cd2eabb2b098b446e64391e419c5e..eb8468ba5b6f3a0c08a68d8587a0011531ee3c54 100644 (file)
 package org.sonar.plugins.core;
 
 import com.google.common.collect.ImmutableList;
-import org.sonar.api.*;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.Properties;
+import org.sonar.api.Property;
+import org.sonar.api.PropertyType;
+import org.sonar.api.SonarPlugin;
 import org.sonar.api.checks.NoSonarFilter;
 import org.sonar.core.timemachine.Periods;
 import org.sonar.plugins.core.batch.IndexProjectPostJob;
@@ -28,19 +32,89 @@ import org.sonar.plugins.core.charts.DistributionAreaChart;
 import org.sonar.plugins.core.charts.DistributionBarChart;
 import org.sonar.plugins.core.charts.XradarChart;
 import org.sonar.plugins.core.colorizers.JavaColorizerFormat;
-import org.sonar.plugins.core.dashboards.*;
-import org.sonar.plugins.core.issue.*;
-import org.sonar.plugins.core.issue.notification.*;
+import org.sonar.plugins.core.dashboards.GlobalDefaultDashboard;
+import org.sonar.plugins.core.dashboards.ProjectDefaultDashboard;
+import org.sonar.plugins.core.dashboards.ProjectHotspotDashboard;
+import org.sonar.plugins.core.dashboards.ProjectIssuesDashboard;
+import org.sonar.plugins.core.dashboards.ProjectTimeMachineDashboard;
+import org.sonar.plugins.core.issue.CountFalsePositivesDecorator;
+import org.sonar.plugins.core.issue.CountUnresolvedIssuesDecorator;
+import org.sonar.plugins.core.issue.InitialOpenIssuesSensor;
+import org.sonar.plugins.core.issue.InitialOpenIssuesStack;
+import org.sonar.plugins.core.issue.IssueHandlers;
+import org.sonar.plugins.core.issue.IssueTracking;
+import org.sonar.plugins.core.issue.IssueTrackingDecorator;
+import org.sonar.plugins.core.issue.IssuesDensityDecorator;
+import org.sonar.plugins.core.issue.WeightedIssuesDecorator;
+import org.sonar.plugins.core.issue.notification.ChangesOnMyIssueNotificationDispatcher;
+import org.sonar.plugins.core.issue.notification.IssueChangesEmailTemplate;
+import org.sonar.plugins.core.issue.notification.NewFalsePositiveNotificationDispatcher;
+import org.sonar.plugins.core.issue.notification.NewIssuesEmailTemplate;
+import org.sonar.plugins.core.issue.notification.NewIssuesNotificationDispatcher;
+import org.sonar.plugins.core.issue.notification.SendIssueNotificationsPostJob;
 import org.sonar.plugins.core.measurefilters.MyFavouritesFilter;
 import org.sonar.plugins.core.measurefilters.ProjectFilter;
 import org.sonar.plugins.core.notifications.alerts.NewAlerts;
 import org.sonar.plugins.core.security.ApplyProjectRolesDecorator;
-import org.sonar.plugins.core.sensors.*;
-import org.sonar.plugins.core.timemachine.*;
+import org.sonar.plugins.core.sensors.BranchCoverageDecorator;
+import org.sonar.plugins.core.sensors.CommentDensityDecorator;
+import org.sonar.plugins.core.sensors.CoverageDecorator;
+import org.sonar.plugins.core.sensors.CoverageMeasurementFilter;
+import org.sonar.plugins.core.sensors.DirectoriesDecorator;
+import org.sonar.plugins.core.sensors.FileHashSensor;
+import org.sonar.plugins.core.sensors.FilesDecorator;
+import org.sonar.plugins.core.sensors.ItBranchCoverageDecorator;
+import org.sonar.plugins.core.sensors.ItCoverageDecorator;
+import org.sonar.plugins.core.sensors.ItLineCoverageDecorator;
+import org.sonar.plugins.core.sensors.LineCoverageDecorator;
+import org.sonar.plugins.core.sensors.ManualMeasureDecorator;
+import org.sonar.plugins.core.sensors.OverallBranchCoverageDecorator;
+import org.sonar.plugins.core.sensors.OverallCoverageDecorator;
+import org.sonar.plugins.core.sensors.OverallLineCoverageDecorator;
+import org.sonar.plugins.core.sensors.ProjectLinksSensor;
+import org.sonar.plugins.core.sensors.UnitTestDecorator;
+import org.sonar.plugins.core.sensors.VersionEventsSensor;
+import org.sonar.plugins.core.timemachine.NewCoverageAggregator;
+import org.sonar.plugins.core.timemachine.NewCoverageFileAnalyzer;
+import org.sonar.plugins.core.timemachine.NewItCoverageFileAnalyzer;
+import org.sonar.plugins.core.timemachine.NewOverallCoverageFileAnalyzer;
+import org.sonar.plugins.core.timemachine.TendencyDecorator;
+import org.sonar.plugins.core.timemachine.TimeMachineConfigurationPersister;
+import org.sonar.plugins.core.timemachine.VariationDecorator;
 import org.sonar.plugins.core.web.TestsViewer;
-import org.sonar.plugins.core.widgets.*;
-import org.sonar.plugins.core.widgets.issues.*;
-import org.sonar.plugins.core.widgets.measures.*;
+import org.sonar.plugins.core.widgets.AlertsWidget;
+import org.sonar.plugins.core.widgets.BubbleChartWidget;
+import org.sonar.plugins.core.widgets.ComplexityWidget;
+import org.sonar.plugins.core.widgets.CoverageWidget;
+import org.sonar.plugins.core.widgets.CustomMeasuresWidget;
+import org.sonar.plugins.core.widgets.DescriptionWidget;
+import org.sonar.plugins.core.widgets.DocumentationCommentsWidget;
+import org.sonar.plugins.core.widgets.DuplicationsWidget;
+import org.sonar.plugins.core.widgets.EventsWidget;
+import org.sonar.plugins.core.widgets.HotspotMetricWidget;
+import org.sonar.plugins.core.widgets.HotspotMostViolatedResourcesWidget;
+import org.sonar.plugins.core.widgets.HotspotMostViolatedRulesWidget;
+import org.sonar.plugins.core.widgets.ItCoverageWidget;
+import org.sonar.plugins.core.widgets.ProjectFileCloudWidget;
+import org.sonar.plugins.core.widgets.SizeWidget;
+import org.sonar.plugins.core.widgets.TechnicalDebtPyramidWidget;
+import org.sonar.plugins.core.widgets.TimeMachineWidget;
+import org.sonar.plugins.core.widgets.TimelineWidget;
+import org.sonar.plugins.core.widgets.TreemapWidget;
+import org.sonar.plugins.core.widgets.WelcomeWidget;
+import org.sonar.plugins.core.widgets.issues.ActionPlansWidget;
+import org.sonar.plugins.core.widgets.issues.FalsePositiveIssuesWidget;
+import org.sonar.plugins.core.widgets.issues.IssueFilterWidget;
+import org.sonar.plugins.core.widgets.issues.IssuesWidget;
+import org.sonar.plugins.core.widgets.issues.MyUnresolvedIssuesWidget;
+import org.sonar.plugins.core.widgets.issues.UnresolvedIssuesPerAssigneeWidget;
+import org.sonar.plugins.core.widgets.issues.UnresolvedIssuesStatusesWidget;
+import org.sonar.plugins.core.widgets.measures.MeasureFilterAsBubbleChartWidget;
+import org.sonar.plugins.core.widgets.measures.MeasureFilterAsCloudWidget;
+import org.sonar.plugins.core.widgets.measures.MeasureFilterAsHistogramWidget;
+import org.sonar.plugins.core.widgets.measures.MeasureFilterAsPieChartWidget;
+import org.sonar.plugins.core.widgets.measures.MeasureFilterListWidget;
+import org.sonar.plugins.core.widgets.measures.MeasureFilterTreemapWidget;
 
 import java.util.List;
 
@@ -285,7 +359,6 @@ public final class CorePlugin extends SonarPlugin {
       NewFalsePositiveNotificationDispatcher.newMetadata(),
 
       // batch
-      ProfileEventsSensor.class,
       ProjectLinksSensor.class,
       UnitTestDecorator.class,
       VersionEventsSensor.class,
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileEventsSensor.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileEventsSensor.java
deleted file mode 100644 (file)
index c21c630..0000000
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * 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.plugins.core.sensors;
-
-import org.sonar.api.batch.*;
-import org.sonar.api.batch.fs.FileSystem;
-import org.sonar.api.measures.CoreMetrics;
-import org.sonar.api.measures.Measure;
-import org.sonar.api.measures.Metric;
-import org.sonar.api.profiles.RulesProfile;
-import org.sonar.api.resources.Project;
-import org.sonar.batch.rule.RulesProfileWrapper;
-
-import java.util.List;
-
-public class ProfileEventsSensor implements Sensor {
-
-  private final RulesProfile profile;
-  private final TimeMachine timeMachine;
-  private final FileSystem fs;
-
-  public ProfileEventsSensor(RulesProfile profile, TimeMachine timeMachine, FileSystem fs) {
-    this.profile = profile;
-    this.timeMachine = timeMachine;
-    this.fs = fs;
-  }
-
-  public boolean shouldExecuteOnProject(Project project) {
-    // Views will define a fake profile
-    return profile instanceof RulesProfileWrapper;
-  }
-
-  public void analyse(Project project, SensorContext context) {
-    RulesProfileWrapper profileWrapper = (RulesProfileWrapper) profile;
-    for (String languageKey : fs.languages()) {
-      RulesProfile realProfile = profileWrapper.getProfileByLanguage(languageKey);
-      Measure pastProfileMeasure = getPreviousMeasure(project, CoreMetrics.PROFILE);
-      if (pastProfileMeasure == null) {
-        // first analysis
-        return;
-      }
-      int pastProfileId = pastProfileMeasure.getIntValue();
-      Measure pastProfileVersionMeasure = getPreviousMeasure(project, CoreMetrics.PROFILE_VERSION);
-      final int pastProfileVersion;
-      // first analysis with versions
-      if (pastProfileVersionMeasure == null) {
-        pastProfileVersion = 1;
-      } else {
-        pastProfileVersion = pastProfileVersionMeasure.getIntValue();
-      }
-      String pastProfile = formatProfileDescription(pastProfileMeasure.getData(), pastProfileVersion);
-
-      int currentProfileId = realProfile.getId();
-      int currentProfileVersion = realProfile.getVersion();
-      String currentProfile = formatProfileDescription(realProfile.getName(), currentProfileVersion);
-
-      if ((pastProfileId != currentProfileId) || (pastProfileVersion != currentProfileVersion)) {
-        // A different profile is used for this project or new version of same profile
-        context.createEvent(project, currentProfile, currentProfile + " is used instead of " + pastProfile, Event.CATEGORY_PROFILE, null);
-      }
-    }
-  }
-
-  private static String formatProfileDescription(String name, int version) {
-    return name + " version " + version;
-  }
-
-  private Measure getPreviousMeasure(Project project, Metric metric) {
-    TimeMachineQuery query = new TimeMachineQuery(project)
-      .setOnlyLastAnalysis(true)
-      .setMetrics(metric);
-    List<Measure> measures = timeMachine.getMeasures(query);
-    if (measures.isEmpty()) {
-      return null;
-    }
-    return measures.get(0);
-  }
-
-  @Override
-  public String toString() {
-    return getClass().getSimpleName();
-  }
-}
index 77931f40854824a4b86b7c421b7c6a606d22d3d7..1d0fcb9c40987b69b10fd22a95932818d02216f1 100644 (file)
@@ -25,7 +25,7 @@
       <td><%= message('widget.description.profiles') -%>:</td>
       <td><span id="resource_profile">
         <% profiles.each_with_index do |profile, i| %>
-          <%= Api::Utils.language_name(profile['language']) -%>: <%= link_to profile['name'], {:controller => '/rules_configuration', :action => 'index', :id => profile['id']}, :id => profile['language'] + '_profile_link' -%></span> (<%= message('widget.description.profile_version_x', :params => profile['version']) -%>)
+          <%= Api::Utils.language_name(profile['language']) -%>: <%= link_to profile['name'], {:controller => '/profiles', :action => 'show', :id => profile['id']}, :id => profile['language'] + '_profile_link' -%></span> (<%= message('widget.description.profile_version_x', :params => profile['version']) -%>)
           <% if i < (profiles.size - 1) %>
           <br/>
           <% end %>
@@ -39,7 +39,7 @@
     %>
     <tr>
       <td><%= message('widget.description.profile') -%>:</td>
-      <td><span id="resource_profile"><%= link_to profile_measure.data, {:controller => '/rules_configuration', :action => 'index', :id => profile_measure.value.to_i}, :id => 'profile_link' -%></span> (<%= message('widget.description.profile_version_x', :params => format_measure('profile_version', :default => '1')) -%>)</td>
+      <td><span id="resource_profile"><%= link_to profile_measure.data, {:controller => '/profiles', :action => 'show', :id => profile_measure.value.to_i}, :id => 'profile_link' -%></span> (<%= message('widget.description.profile_version_x', :params => format_measure('profile_version', :default => '1')) -%>)</td>
     </tr>
     <%   end
       end %>
diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileEventsSensorTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileEventsSensorTest.java
deleted file mode 100644 (file)
index 41e5040..0000000
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * 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.plugins.core.sensors;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.sonar.api.batch.Event;
-import org.sonar.api.batch.SensorContext;
-import org.sonar.api.batch.TimeMachine;
-import org.sonar.api.batch.TimeMachineQuery;
-import org.sonar.api.batch.fs.FileSystem;
-import org.sonar.api.batch.fs.internal.DefaultFileSystem;
-import org.sonar.api.measures.CoreMetrics;
-import org.sonar.api.measures.Measure;
-import org.sonar.api.profiles.RulesProfile;
-import org.sonar.api.resources.Project;
-import org.sonar.batch.rule.RulesProfileWrapper;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Date;
-
-import static org.fest.assertions.Assertions.assertThat;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.same;
-import static org.mockito.Mockito.*;
-
-public class ProfileEventsSensorTest {
-
-  Project project;
-  SensorContext context;
-  FileSystem fs;
-  RulesProfileWrapper wrapper;
-  RulesProfile profile;
-
-  @Before
-  public void prepare() {
-    project = mock(Project.class);
-    context = mock(SensorContext.class);
-
-    fs = new DefaultFileSystem().addLanguages("java");
-    profile = mock(RulesProfile.class);
-    when(profile.getLanguage()).thenReturn("java");
-    wrapper = new RulesProfileWrapper(profile);
-  }
-
-  @Test
-  public void shouldExecuteWhenProfileWithId() {
-    when(profile.getId()).thenReturn(123);
-    ProfileEventsSensor sensor = new ProfileEventsSensor(wrapper, null, fs);
-
-    assertThat(sensor.shouldExecuteOnProject(project)).isTrue();
-    verifyZeroInteractions(project);
-  }
-
-  @Test
-  public void shouldNotExecuteIfProfileIsNotWrapper() {
-    RulesProfile profile = mock(RulesProfile.class);
-    when(profile.getId()).thenReturn(null);
-    ProfileEventsSensor sensor = new ProfileEventsSensor(profile, null, fs);
-
-    assertThat(sensor.shouldExecuteOnProject(project)).isFalse();
-    verifyZeroInteractions(project);
-  }
-
-  @Test
-  public void shouldDoNothingIfNoProfileChange() {
-    mockProfileWithVersion(1);
-    TimeMachine timeMachine = mockTM(22.0, "Foo", 1.0); // Same profile, same version
-    ProfileEventsSensor sensor = new ProfileEventsSensor(wrapper, timeMachine, fs);
-
-    sensor.analyse(project, context);
-
-    verifyZeroInteractions(context);
-  }
-
-  @Test
-  public void shouldCreateEventIfProfileChange() {
-    mockProfileWithVersion(1);
-    TimeMachine timeMachine = mockTM(21.0, "Bar", 1.0); // Different profile
-    ProfileEventsSensor sensor = new ProfileEventsSensor(wrapper, timeMachine, fs);
-
-    sensor.analyse(project, context);
-
-    verify(context).createEvent(same(project),
-        eq("Foo version 1"),
-        eq("Foo version 1 is used instead of Bar version 1"),
-        same(Event.CATEGORY_PROFILE), any(Date.class));
-  }
-
-  @Test
-  public void shouldCreateEventIfProfileVersionChange() {
-    mockProfileWithVersion(2);
-    TimeMachine timeMachine = mockTM(22.0, "Foo", 1.0); // Same profile, different version
-    ProfileEventsSensor sensor = new ProfileEventsSensor(wrapper, timeMachine, fs);
-
-    sensor.analyse(project, context);
-
-    verify(context).createEvent(same(project),
-        eq("Foo version 2"),
-        eq("Foo version 2 is used instead of Foo version 1"),
-        same(Event.CATEGORY_PROFILE), any(Date.class));
-  }
-
-  @Test
-  public void shouldNotCreateEventIfFirstAnalysis() {
-    mockProfileWithVersion(2);
-    TimeMachine timeMachine = mockTM(null, null);
-    ProfileEventsSensor sensor = new ProfileEventsSensor(wrapper, timeMachine, fs);
-
-    sensor.analyse(project, context);
-
-    verifyZeroInteractions(context);
-  }
-
-  @Test
-  public void shouldCreateEventIfFirstAnalysisWithVersionsAndVersionMoreThan1() {
-    mockProfileWithVersion(2);
-    TimeMachine timeMachine = mockTM(22.0, "Foo", null);
-    ProfileEventsSensor sensor = new ProfileEventsSensor(wrapper, timeMachine, fs);
-
-    sensor.analyse(project, context);
-
-    verify(context).createEvent(same(project),
-        eq("Foo version 2"),
-        eq("Foo version 2 is used instead of Foo version 1"),
-        same(Event.CATEGORY_PROFILE), any(Date.class));
-  }
-
-  private void mockProfileWithVersion(int version) {
-    when(profile.getId()).thenReturn(22);
-    when(profile.getName()).thenReturn("Foo");
-    when(profile.getVersion()).thenReturn(version);
-  }
-
-  private TimeMachine mockTM(double profileId, String profileName, Double versionValue) {
-    return mockTM(new Measure(CoreMetrics.PROFILE, profileId, profileName), versionValue == null ? null : new Measure(CoreMetrics.PROFILE_VERSION, versionValue));
-  }
-
-  private TimeMachine mockTM(Measure result1, Measure result2) {
-    TimeMachine timeMachine = mock(TimeMachine.class);
-
-    when(timeMachine.getMeasures(any(TimeMachineQuery.class)))
-        .thenReturn(result1 == null ? Collections.<Measure>emptyList() : Arrays.asList(result1))
-        .thenReturn(result2 == null ? Collections.<Measure>emptyList() : Arrays.asList(result2));
-
-    return timeMachine;
-  }
-}
index 7c31d566b4c7bbd701c32f5a154b91de3b893b9f..b51dcf8a9c035917185f211ff26c2137d44cd801 100644 (file)
@@ -21,8 +21,10 @@ package org.sonar.batch.rule;
 
 import org.sonar.api.batch.Decorator;
 import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.DependedUpon;
 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.Resource;
 import org.sonar.api.resources.ResourceUtils;
@@ -32,6 +34,11 @@ import org.sonar.api.resources.ResourceUtils;
  */
 public class QProfileDecorator implements Decorator {
 
+  @DependedUpon
+  public Metric provides() {
+    return CoreMetrics.PROFILES;
+  }
+
   public boolean shouldExecuteOnProject(Project project) {
     return project.getModules().size() > 0;
   }
diff --git a/sonar-batch/src/main/java/org/sonar/batch/rule/QProfileEventsDecorator.java b/sonar-batch/src/main/java/org/sonar/batch/rule/QProfileEventsDecorator.java
new file mode 100644 (file)
index 0000000..d2a79e8
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+ * 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.batch.rule;
+
+import com.google.common.collect.Maps;
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.DependsUpon;
+import org.sonar.api.batch.Event;
+import org.sonar.api.batch.TimeMachine;
+import org.sonar.api.batch.TimeMachineQuery;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.ResourceUtils;
+import org.sonar.batch.rule.ModuleQProfiles.QProfile;
+import org.sonar.core.qualityprofile.db.QualityProfileDao;
+import org.sonar.core.qualityprofile.db.QualityProfileDto;
+
+import java.util.List;
+import java.util.Map;
+
+public class QProfileEventsDecorator implements Decorator {
+
+  private final TimeMachine timeMachine;
+  private final QualityProfileDao qualityProfileDao;
+  private final Languages languages;
+
+  public QProfileEventsDecorator(TimeMachine timeMachine, QualityProfileDao qualityProfileDao, Languages languages) {
+    this.timeMachine = timeMachine;
+    this.qualityProfileDao = qualityProfileDao;
+    this.languages = languages;
+  }
+
+  @DependsUpon
+  public Metric dependsUpon() {
+    return CoreMetrics.PROFILES;
+  }
+
+  public boolean shouldExecuteOnProject(Project project) {
+    return true;
+  }
+
+  @Override
+  public void decorate(Resource resource, DecoratorContext context) {
+    if (!ResourceUtils.isProject(resource)) {
+      return;
+    }
+
+    // Load current profiles
+    Measure profilesMeasure = context.getMeasure(CoreMetrics.PROFILES);
+    UsedQProfiles currentProfiles = UsedQProfiles.fromJSON(profilesMeasure.getData());
+
+    // Now load previous profiles
+    UsedQProfiles pastProfiles;
+    // First try with new metric
+    Measure pastProfilesMeasure = getPreviousMeasure(resource, CoreMetrics.PROFILES);
+    if (pastProfilesMeasure != null) {
+      pastProfiles = UsedQProfiles.fromJSON(pastProfilesMeasure.getData());
+    } else {
+      // Fallback to old metric
+      Measure pastProfileMeasure = getPreviousMeasure(resource, CoreMetrics.PROFILE);
+      if (pastProfileMeasure == null) {
+        // first analysis
+        return;
+      }
+      int pastProfileId = pastProfileMeasure.getIntValue();
+      String pastProfileName = pastProfileMeasure.getData();
+      QualityProfileDto pastProfile = qualityProfileDao.selectById(pastProfileId);
+      String pastProfileLanguage = "unknow";
+      if (pastProfile != null) {
+        pastProfileLanguage = pastProfile.getLanguage();
+      }
+      Measure pastProfileVersionMeasure = getPreviousMeasure(resource, CoreMetrics.PROFILE_VERSION);
+      final int pastProfileVersion;
+      // first analysis with versions
+      if (pastProfileVersionMeasure == null) {
+        pastProfileVersion = 1;
+      } else {
+        pastProfileVersion = pastProfileVersionMeasure.getIntValue();
+      }
+      pastProfiles = UsedQProfiles.fromProfiles(new ModuleQProfiles.QProfile(pastProfileId, pastProfileName, pastProfileLanguage, pastProfileVersion));
+    }
+
+    // Now create appropriate events
+    Map<Integer, QProfile> pastProfilesById = Maps.newHashMap(pastProfiles.profilesById());
+    for (QProfile profile : currentProfiles.profilesById().values()) {
+      if (pastProfilesById.containsKey(profile.id())) {
+        QProfile pastProfile = pastProfilesById.get(profile.id());
+        if (pastProfile.version() < profile.version()) {
+          // New version of the same QP
+          usedProfile(context, profile);
+        }
+        pastProfilesById.remove(profile.id());
+      } else {
+        usedProfile(context, profile);
+      }
+    }
+    for (QProfile profile : pastProfilesById.values()) {
+      // Following profiles are no more used
+      stopUsedProfile(context, profile);
+    }
+  }
+
+  private void stopUsedProfile(DecoratorContext context, QProfile profile) {
+    Language language = languages.get(profile.language());
+    String languageName = language != null ? language.getName() : profile.language();
+    context.createEvent("Stop using " + format(profile) + " (" + languageName + ")", format(profile) + " no more used for " + languageName, Event.CATEGORY_PROFILE, null);
+  }
+
+  private void usedProfile(DecoratorContext context, QProfile profile) {
+    Language language = languages.get(profile.language());
+    String languageName = language != null ? language.getName() : profile.language();
+    context.createEvent("Use " + format(profile) + " (" + languageName + ")", format(profile) + " used for " + languageName, Event.CATEGORY_PROFILE, null);
+  }
+
+  private String format(QProfile profile) {
+    return profile.name() + " version " + profile.version();
+  }
+
+  private Measure getPreviousMeasure(Resource project, Metric metric) {
+    TimeMachineQuery query = new TimeMachineQuery(project)
+      .setOnlyLastAnalysis(true)
+      .setMetrics(metric);
+    List<Measure> measures = timeMachine.getMeasures(query);
+    if (measures.isEmpty()) {
+      return null;
+    }
+    return measures.get(0);
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName();
+  }
+}
index 13893c64811c69c04f7d493bcc3ec49860efc6af..1b1115ed18bd5ea208983a885d6968f50a13b975 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.batch.rule;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
@@ -36,7 +37,7 @@ import java.util.Map;
 @Immutable
 public class UsedQProfiles {
 
-  private Map<String, Map<Integer, ModuleQProfiles.QProfile>> profilesByLanguage = Maps.newLinkedHashMap();
+  private Map<Integer, ModuleQProfiles.QProfile> profilesById = Maps.newLinkedHashMap();
 
   private UsedQProfiles() {
   }
@@ -71,15 +72,13 @@ public class UsedQProfiles {
     StringWriter json = new StringWriter();
     JsonWriter writer = JsonWriter.of(json);
     writer.beginArray();
-    for (String languageKey : profilesByLanguage.keySet()) {
-      for (ModuleQProfiles.QProfile qProfile : profilesByLanguage.get(languageKey).values()) {
-        writer.beginObject()
-          .prop("id", qProfile.id())
-          .prop("name", qProfile.name())
-          .prop("version", qProfile.version())
-          .prop("language", qProfile.language())
-          .endObject();
-      }
+    for (ModuleQProfiles.QProfile qProfile : profilesById.values()) {
+      writer.beginObject()
+        .prop("id", qProfile.id())
+        .prop("name", qProfile.name())
+        .prop("version", qProfile.version())
+        .prop("language", qProfile.language())
+        .endObject();
     }
     writer.endArray();
     writer.close();
@@ -91,14 +90,11 @@ public class UsedQProfiles {
   }
 
   private void add(ModuleQProfiles.QProfile profile) {
-    if (!profilesByLanguage.containsKey(profile.language())) {
-      profilesByLanguage.put(profile.language(), Maps.<Integer, ModuleQProfiles.QProfile>newLinkedHashMap());
-    }
-    QProfile alreadyAdded = profilesByLanguage.get(profile.language()).get(profile.id());
+    QProfile alreadyAdded = profilesById.get(profile.id());
     if (alreadyAdded == null
       // Keep only latest version
       || profile.version() > alreadyAdded.version()) {
-      profilesByLanguage.get(profile.language()).put(profile.id(), profile);
+      profilesById.put(profile.id(), profile);
     }
   }
 
@@ -110,10 +106,12 @@ public class UsedQProfiles {
   }
 
   private UsedQProfiles mergeInPlace(UsedQProfiles other) {
-    for (Map<Integer, QProfile> byIds : other.profilesByLanguage.values()) {
-      this.addAll(byIds.values());
-    }
+    this.addAll(other.profilesById.values());
     return this;
   }
 
+  public Map<Integer, ModuleQProfiles.QProfile> profilesById() {
+    return ImmutableMap.copyOf(profilesById);
+  }
+
 }
index c2fb81ba9825fa49fde0f9ad8599ea569c0e5c78..fc5da97d56da3169aed013e5cf2df70a06d11dfe 100644 (file)
@@ -60,6 +60,7 @@ import org.sonar.batch.qualitygate.QualityGateVerifier;
 import org.sonar.batch.rule.ActiveRulesProvider;
 import org.sonar.batch.rule.ModuleQProfiles;
 import org.sonar.batch.rule.QProfileDecorator;
+import org.sonar.batch.rule.QProfileEventsDecorator;
 import org.sonar.batch.rule.QProfileSensor;
 import org.sonar.batch.rule.QProfileVerifier;
 import org.sonar.batch.rule.RulesProfileProvider;
@@ -157,6 +158,7 @@ public class ModuleScanContainer extends ComponentContainer {
       new RulesProfileProvider(),
       QProfileSensor.class,
       QProfileDecorator.class,
+      QProfileEventsDecorator.class,
       CheckFactory.class,
 
       // report
diff --git a/sonar-batch/src/test/java/org/sonar/batch/rule/QProfileEventsDecoratorTest.java b/sonar-batch/src/test/java/org/sonar/batch/rule/QProfileEventsDecoratorTest.java
new file mode 100644 (file)
index 0000000..3fe9497
--- /dev/null
@@ -0,0 +1,213 @@
+/*
+ * 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.batch.rule;
+
+import org.junit.Test;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.Event;
+import org.sonar.api.batch.TimeMachine;
+import org.sonar.api.batch.TimeMachineQuery;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.resources.Java;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.resources.Project;
+import org.sonar.core.qualityprofile.db.QualityProfileDao;
+import org.sonar.core.qualityprofile.db.QualityProfileDto;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class QProfileEventsDecoratorTest {
+
+  Project project = new Project("myProject");
+  DecoratorContext decoratorContext = mock(DecoratorContext.class);
+  TimeMachine timeMachine = mock(TimeMachine.class);
+  private Languages languages = mock(Languages.class);
+  private QualityProfileDao qualityProfileDao = mock(QualityProfileDao.class);
+  QProfileEventsDecorator decorator = new QProfileEventsDecorator(timeMachine, qualityProfileDao, languages);
+
+  @Test
+  public void shouldExecuteOnProjects() {
+    assertThat(decorator.shouldExecuteOnProject(project)).isTrue();
+  }
+
+  @Test
+  public void shouldDoNothingIfNoProfileChange() {
+    Measure previousMeasure = new Measure(CoreMetrics.PROFILES, "[{\"id\":2,\"name\":\"Java Two\",\"version\":20,\"language\":\"java\"}]");
+    Measure newMeasure = new Measure(CoreMetrics.PROFILES, "[{\"id\":2,\"name\":\"Java Two\",\"version\":20,\"language\":\"java\"}]");
+
+    when(timeMachine.getMeasures(any(TimeMachineQuery.class)))
+      .thenReturn(Arrays.asList(previousMeasure));
+    when(decoratorContext.getMeasure(CoreMetrics.PROFILES)).thenReturn(newMeasure);
+
+    decorator.decorate(project, decoratorContext);
+
+    verify(decoratorContext, never()).createEvent(anyString(), anyString(), anyString(), any(Date.class));
+  }
+
+  @Test
+  public void shouldDoNothingIfNoProfileChange_fallbackOldProfileMeasure() {
+    mockTMWithDeprecatedProfileMeasures(2, "Java Two", 20);
+    when(qualityProfileDao.selectById(20)).thenReturn(new QualityProfileDto().setLanguage("java"));
+    Measure newMeasure = new Measure(CoreMetrics.PROFILES, "[{\"id\":2,\"name\":\"Java Two\",\"version\":20,\"language\":\"java\"}]");
+
+    when(decoratorContext.getMeasure(CoreMetrics.PROFILES)).thenReturn(newMeasure);
+
+    when(languages.get("java")).thenReturn(Java.INSTANCE);
+
+    decorator.decorate(project, decoratorContext);
+
+    verify(decoratorContext, never()).createEvent(anyString(), anyString(), anyString(), any(Date.class));
+  }
+
+  @Test
+  public void shouldCreateEventIfProfileChange() {
+    Measure previousMeasure = new Measure(CoreMetrics.PROFILES, "[{\"id\":2,\"name\":\"Java Two\",\"version\":20,\"language\":\"java\"}]");
+    // Different profile
+    Measure newMeasure = new Measure(CoreMetrics.PROFILES, "[{\"id\":3,\"name\":\"Java Other\",\"version\":1,\"language\":\"java\"}]");
+
+    when(timeMachine.getMeasures(any(TimeMachineQuery.class)))
+      .thenReturn(Arrays.asList(previousMeasure));
+    when(decoratorContext.getMeasure(CoreMetrics.PROFILES)).thenReturn(newMeasure);
+
+    when(languages.get("java")).thenReturn(Java.INSTANCE);
+
+    decorator.decorate(project, decoratorContext);
+
+    verify(decoratorContext).createEvent(
+      eq("Use Java Other version 1 (Java)"),
+      eq("Java Other version 1 used for Java"),
+      same(Event.CATEGORY_PROFILE), any(Date.class));
+  }
+
+  @Test
+  public void shouldCreateEventIfProfileChange_fallbackOldProfileMeasure() {
+    mockTMWithDeprecatedProfileMeasures(2, "Java Two", 20);
+    when(qualityProfileDao.selectById(20)).thenReturn(new QualityProfileDto().setLanguage("java"));
+    // Different profile
+    Measure newMeasure = new Measure(CoreMetrics.PROFILES, "[{\"id\":3,\"name\":\"Java Other\",\"version\":1,\"language\":\"java\"}]");
+
+    when(decoratorContext.getMeasure(CoreMetrics.PROFILES)).thenReturn(newMeasure);
+
+    when(languages.get("java")).thenReturn(Java.INSTANCE);
+
+    decorator.decorate(project, decoratorContext);
+
+    verify(decoratorContext).createEvent(
+      eq("Use Java Other version 1 (Java)"),
+      eq("Java Other version 1 used for Java"),
+      same(Event.CATEGORY_PROFILE), any(Date.class));
+  }
+
+  @Test
+  public void shouldCreateEventIfProfileVersionChange() {
+    Measure previousMeasure = new Measure(CoreMetrics.PROFILES, "[{\"id\":2,\"name\":\"Java Two\",\"version\":20,\"language\":\"java\"}]");
+    // Same profile, different version
+    Measure newMeasure = new Measure(CoreMetrics.PROFILES, "[{\"id\":2,\"name\":\"Java Two\",\"version\":21,\"language\":\"java\"}]");
+
+    when(timeMachine.getMeasures(any(TimeMachineQuery.class)))
+      .thenReturn(Arrays.asList(previousMeasure));
+    when(decoratorContext.getMeasure(CoreMetrics.PROFILES)).thenReturn(newMeasure);
+
+    when(languages.get("java")).thenReturn(Java.INSTANCE);
+
+    decorator.decorate(project, decoratorContext);
+
+    verify(decoratorContext).createEvent(
+      eq("Use Java Two version 21 (Java)"),
+      eq("Java Two version 21 used for Java"),
+      same(Event.CATEGORY_PROFILE), any(Date.class));
+  }
+
+  @Test
+  public void shouldCreateEventIfProfileVersionChange_fallbackOldProfileMeasure() {
+    mockTMWithDeprecatedProfileMeasures(2, "Java Two", 20);
+    when(qualityProfileDao.selectById(20)).thenReturn(new QualityProfileDto().setLanguage("java"));
+    // Same profile, different version
+    Measure newMeasure = new Measure(CoreMetrics.PROFILES, "[{\"id\":2,\"name\":\"Java Two\",\"version\":21,\"language\":\"java\"}]");
+
+    when(decoratorContext.getMeasure(CoreMetrics.PROFILES)).thenReturn(newMeasure);
+
+    when(languages.get("java")).thenReturn(Java.INSTANCE);
+
+    decorator.decorate(project, decoratorContext);
+
+    verify(decoratorContext).createEvent(
+      eq("Use Java Two version 21 (Java)"),
+      eq("Java Two version 21 used for Java"),
+      same(Event.CATEGORY_PROFILE), any(Date.class));
+  }
+
+  @Test
+  public void shouldCreateEventIfProfileVersionChange_fallbackOldProfileMeasure_noVersion() {
+    mockTMWithDeprecatedProfileMeasures(2, "Java Two", null);
+    when(qualityProfileDao.selectById(20)).thenReturn(new QualityProfileDto().setLanguage("java"));
+    // Same profile, different version
+    Measure newMeasure = new Measure(CoreMetrics.PROFILES, "[{\"id\":2,\"name\":\"Java Two\",\"version\":21,\"language\":\"java\"}]");
+
+    when(decoratorContext.getMeasure(CoreMetrics.PROFILES)).thenReturn(newMeasure);
+
+    when(languages.get("java")).thenReturn(Java.INSTANCE);
+
+    decorator.decorate(project, decoratorContext);
+
+    verify(decoratorContext).createEvent(
+      eq("Use Java Two version 21 (Java)"),
+      eq("Java Two version 21 used for Java"),
+      same(Event.CATEGORY_PROFILE), any(Date.class));
+  }
+
+  @Test
+  public void shouldNotCreateEventIfFirstAnalysis() {
+    Measure newMeasure = new Measure(CoreMetrics.PROFILES, "[{\"id\":2,\"name\":\"Java Two\",\"version\":21,\"language\":\"java\"}]");
+
+    when(decoratorContext.getMeasure(CoreMetrics.PROFILES)).thenReturn(newMeasure);
+
+    when(languages.get("java")).thenReturn(Java.INSTANCE);
+
+    decorator.decorate(project, decoratorContext);
+
+    verify(decoratorContext, never()).createEvent(anyString(), anyString(), anyString(), any(Date.class));
+  }
+
+  private void mockTMWithDeprecatedProfileMeasures(double profileId, String profileName, Integer versionValue) {
+    mockTM(new Measure(CoreMetrics.PROFILE, profileId, profileName), versionValue == null ? null : new Measure(CoreMetrics.PROFILE_VERSION, Double.valueOf(versionValue)));
+  }
+
+  private void mockTM(Measure result1, Measure result2) {
+    when(timeMachine.getMeasures(any(TimeMachineQuery.class)))
+      .thenReturn(Collections.<Measure>emptyList())
+      .thenReturn(result1 == null ? Collections.<Measure>emptyList() : Arrays.asList(result1))
+      .thenReturn(result2 == null ? Collections.<Measure>emptyList() : Arrays.asList(result2));
+
+  }
+}