From 3bd0d648f1ea9b8d8724ec0cd2b3a8d13ff90951 Mon Sep 17 00:00:00 2001 From: Julien HENRY Date: Mon, 19 May 2014 10:50:08 +0200 Subject: [PATCH] SONAR-5216 Generate profile events for multi language projects --- .../org/sonar/plugins/core/CorePlugin.java | 93 +++++++- .../core/sensors/ProfileEventsSensor.java | 100 -------- .../plugins/core/widgets/description.html.erb | 4 +- .../core/sensors/ProfileEventsSensorTest.java | 167 -------------- .../sonar/batch/rule/QProfileDecorator.java | 7 + .../batch/rule/QProfileEventsDecorator.java | 157 +++++++++++++ .../org/sonar/batch/rule/UsedQProfiles.java | 34 ++- .../sonar/batch/scan/ModuleScanContainer.java | 2 + .../rule/QProfileEventsDecoratorTest.java | 213 ++++++++++++++++++ 9 files changed, 480 insertions(+), 297 deletions(-) delete mode 100644 plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileEventsSensor.java delete mode 100644 plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileEventsSensorTest.java create mode 100644 sonar-batch/src/main/java/org/sonar/batch/rule/QProfileEventsDecorator.java create mode 100644 sonar-batch/src/test/java/org/sonar/batch/rule/QProfileEventsDecoratorTest.java diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java index 6a7499e2332..eb8468ba5b6 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java @@ -20,7 +20,11 @@ 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 index c21c6304458..00000000000 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileEventsSensor.java +++ /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 measures = timeMachine.getMeasures(query); - if (measures.isEmpty()) { - return null; - } - return measures.get(0); - } - - @Override - public String toString() { - return getClass().getSimpleName(); - } -} diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/description.html.erb b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/description.html.erb index 77931f40854..1d0fcb9c409 100644 --- a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/description.html.erb +++ b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/description.html.erb @@ -25,7 +25,7 @@ <%= message('widget.description.profiles') -%>: <% 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' -%> (<%= 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' -%> (<%= message('widget.description.profile_version_x', :params => profile['version']) -%>) <% if i < (profiles.size - 1) %>
<% end %> @@ -39,7 +39,7 @@ %> <%= message('widget.description.profile') -%>: - <%= link_to profile_measure.data, {:controller => '/rules_configuration', :action => 'index', :id => profile_measure.value.to_i}, :id => 'profile_link' -%> (<%= message('widget.description.profile_version_x', :params => format_measure('profile_version', :default => '1')) -%>) + <%= link_to profile_measure.data, {:controller => '/profiles', :action => 'show', :id => profile_measure.value.to_i}, :id => 'profile_link' -%> (<%= message('widget.description.profile_version_x', :params => format_measure('profile_version', :default => '1')) -%>) <% 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 index 41e50402699..00000000000 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileEventsSensorTest.java +++ /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.emptyList() : Arrays.asList(result1)) - .thenReturn(result2 == null ? Collections.emptyList() : Arrays.asList(result2)); - - return timeMachine; - } -} diff --git a/sonar-batch/src/main/java/org/sonar/batch/rule/QProfileDecorator.java b/sonar-batch/src/main/java/org/sonar/batch/rule/QProfileDecorator.java index 7c31d566b4c..b51dcf8a9c0 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/rule/QProfileDecorator.java +++ b/sonar-batch/src/main/java/org/sonar/batch/rule/QProfileDecorator.java @@ -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 index 00000000000..d2a79e8275e --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/rule/QProfileEventsDecorator.java @@ -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 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 measures = timeMachine.getMeasures(query); + if (measures.isEmpty()) { + return null; + } + return measures.get(0); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/rule/UsedQProfiles.java b/sonar-batch/src/main/java/org/sonar/batch/rule/UsedQProfiles.java index 13893c64811..1b1115ed18b 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/rule/UsedQProfiles.java +++ b/sonar-batch/src/main/java/org/sonar/batch/rule/UsedQProfiles.java @@ -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> profilesByLanguage = Maps.newLinkedHashMap(); + private Map 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.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 byIds : other.profilesByLanguage.values()) { - this.addAll(byIds.values()); - } + this.addAll(other.profilesById.values()); return this; } + public Map profilesById() { + return ImmutableMap.copyOf(profilesById); + } + } diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java b/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java index c2fb81ba982..fc5da97d56d 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java +++ b/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java @@ -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 index 00000000000..3fe9497050c --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/rule/QProfileEventsDecoratorTest.java @@ -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.emptyList()) + .thenReturn(result1 == null ? Collections.emptyList() : Arrays.asList(result1)) + .thenReturn(result2 == null ? Collections.emptyList() : Arrays.asList(result2)); + + } +} -- 2.39.5