]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5216 Store and display used quality profiles for multi-language analysis
authorJulien HENRY <julien.henry@sonarsource.com>
Fri, 16 May 2014 14:46:50 +0000 (16:46 +0200)
committerJulien HENRY <julien.henry@sonarsource.com>
Mon, 19 May 2014 09:31:11 +0000 (11:31 +0200)
12 files changed:
plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/description.html.erb
sonar-batch/src/main/java/org/sonar/batch/rule/QProfileDecorator.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/rule/QProfileSensor.java
sonar-batch/src/main/java/org/sonar/batch/rule/UsedQProfiles.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java
sonar-batch/src/test/java/org/sonar/batch/rule/QProfileDecoratorTest.java [new file with mode: 0644]
sonar-batch/src/test/java/org/sonar/batch/rule/QProfileSensorTest.java
sonar-batch/src/test/java/org/sonar/batch/rule/UsedQProfilesTest.java [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties
sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java
sonar-plugin-api/src/test/java/org/sonar/api/resources/CoreMetricsTest.java
sonar-server/src/main/webapp/WEB-INF/app/models/metric.rb

index 2032027f451fdaef5dd5c0b9d29370bb79b46f70..77931f40854824a4b86b7c421b7c6a606d22d3d7 100644 (file)
       <td id="resource_language"><%= Api::Utils.language_name(@project.language) -%></td>
     </tr>
     <% end %>
+    
     <%
-      profile_measure=@snapshot.measure(Metric::PROFILE)
-      if profile_measure
+      profiles_measure = measure(Metric::PROFILES)
+      if profiles_measure && !profiles_measure.data.blank?
+        profiles = JSON.parse profiles_measure.data
+    %>
+    <tr>
+      <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']) -%>)
+          <% if i < (profiles.size - 1) %>
+          <br/>
+          <% end %>
+        <% end %>
+      </td>
+    </tr>
+    <%
+      else
+        profile_measure=@snapshot.measure(Metric::PROFILE)
+        if profile_measure
     %>
     <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>
     </tr>
-    <% end %>
+    <%   end
+      end %>
+    
     <%
       using_default=false
       quality_gate=Property.value('sonar.qualitygate', @resource && @resource.id, nil)
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
new file mode 100644 (file)
index 0000000..7c31d56
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * 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.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.ResourceUtils;
+
+/**
+ * Aggregate which Quality profiles have been used on the current module.
+ */
+public class QProfileDecorator implements Decorator {
+
+  public boolean shouldExecuteOnProject(Project project) {
+    return project.getModules().size() > 0;
+  }
+
+  @Override
+  public void decorate(Resource resource, DecoratorContext context) {
+    if (!ResourceUtils.isProject(resource)) {
+      return;
+    }
+    UsedQProfiles profiles = UsedQProfiles.empty();
+    for (Measure childProfilesMeasure : context.getChildrenMeasures(CoreMetrics.PROFILES)) {
+      UsedQProfiles childProfiles = UsedQProfiles.fromJSON(childProfilesMeasure.getData());
+      profiles = profiles.merge(childProfiles);
+    }
+
+    Measure detailsMeasure = new Measure(CoreMetrics.PROFILES, profiles.toJSON());
+    context.saveMeasure(detailsMeasure);
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName();
+  }
+
+}
index e931163b31ec436b5bf3852e52ee9ac7e600c574..7b44ab64926e728995a8757dbe4721e195cf092b 100644 (file)
@@ -19,7 +19,7 @@
  */
 package org.sonar.batch.rule;
 
-import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 import org.sonar.api.batch.Sensor;
 import org.sonar.api.batch.SensorContext;
 import org.sonar.api.batch.fs.FileSystem;
@@ -28,6 +28,8 @@ import org.sonar.api.measures.Measure;
 import org.sonar.api.resources.Project;
 import org.sonar.core.qualityprofile.db.QualityProfileDao;
 
+import java.util.List;
+
 /**
  * Stores which Quality profiles have been used on the current module.
  */
@@ -44,25 +46,32 @@ public class QProfileSensor implements Sensor {
   }
 
   public boolean shouldExecuteOnProject(Project project) {
-    return true;
+    // Should be only executed on leaf modules
+    return project.getModules().isEmpty();
   }
 
   public void analyse(Project project, SensorContext context) {
+    List<ModuleQProfiles.QProfile> profiles = Lists.newArrayList();
     for (String language : fs.languages()) {
       ModuleQProfiles.QProfile qProfile = moduleQProfiles.findByLanguage(language);
       if (qProfile != null) {
         dao.updateUsedColumn(qProfile.id(), true);
+        profiles.add(qProfile);
       }
     }
-    if (fs.languages().size() == 1) {
-      String language = Iterables.getOnlyElement(fs.languages());
-      ModuleQProfiles.QProfile qProfile = moduleQProfiles.findByLanguage(language);
-      if (qProfile != null) {
-        Measure measure = new Measure(CoreMetrics.PROFILE, qProfile.name()).setValue((double)qProfile.id());
-        Measure measureVersion = new Measure(CoreMetrics.PROFILE_VERSION, qProfile.version().doubleValue());
-        context.saveMeasure(measure);
-        context.saveMeasure(measureVersion);
-      }
+    if (profiles.size() > 0) {
+      UsedQProfiles used = UsedQProfiles.fromProfiles(profiles);
+      Measure detailsMeasure = new Measure(CoreMetrics.PROFILES, used.toJSON());
+      context.saveMeasure(detailsMeasure);
+    }
+
+    // For backward compatibility
+    if (profiles.size() == 1) {
+      ModuleQProfiles.QProfile qProfile = profiles.get(0);
+      Measure measure = new Measure(CoreMetrics.PROFILE, qProfile.name()).setValue((double) qProfile.id());
+      Measure measureVersion = new Measure(CoreMetrics.PROFILE_VERSION, qProfile.version().doubleValue());
+      context.saveMeasure(measure);
+      context.saveMeasure(measureVersion);
     }
   }
 
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
new file mode 100644 (file)
index 0000000..13893c6
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * 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 com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.batch.rule.ModuleQProfiles.QProfile;
+
+import javax.annotation.concurrent.Immutable;
+
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.Map;
+
+@Immutable
+public class UsedQProfiles {
+
+  private Map<String, Map<Integer, ModuleQProfiles.QProfile>> profilesByLanguage = Maps.newLinkedHashMap();
+
+  private UsedQProfiles() {
+  }
+
+  public static final UsedQProfiles fromProfiles(Iterable<QProfile> profiles) {
+    UsedQProfiles result = new UsedQProfiles();
+    for (QProfile qProfile : profiles) {
+      result.add(qProfile);
+    }
+    return result;
+  }
+
+  public static final UsedQProfiles empty() {
+    return new UsedQProfiles();
+  }
+
+  public static final UsedQProfiles fromProfiles(QProfile... profiles) {
+    return fromProfiles(Arrays.asList(profiles));
+  }
+
+  public static final UsedQProfiles fromJSON(String json) {
+    UsedQProfiles result = new UsedQProfiles();
+    JsonArray root = new JsonParser().parse(json).getAsJsonArray();
+    for (JsonElement elt : root) {
+      JsonObject profile = elt.getAsJsonObject();
+      result.add(new QProfile(profile.get("id").getAsInt(), profile.get("name").getAsString(), profile.get("language").getAsString(), profile.get("version").getAsInt()));
+    }
+    return result;
+  }
+
+  public final String toJSON() {
+    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();
+      }
+    }
+    writer.endArray();
+    writer.close();
+    return json.toString();
+  }
+
+  public final UsedQProfiles merge(UsedQProfiles other) {
+    return empty().mergeInPlace(this).mergeInPlace(other);
+  }
+
+  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());
+    if (alreadyAdded == null
+      // Keep only latest version
+      || profile.version() > alreadyAdded.version()) {
+      profilesByLanguage.get(profile.language()).put(profile.id(), profile);
+    }
+  }
+
+  private UsedQProfiles addAll(Iterable<QProfile> profiles) {
+    for (QProfile profile : profiles) {
+      this.add(profile);
+    }
+    return this;
+  }
+
+  private UsedQProfiles mergeInPlace(UsedQProfiles other) {
+    for (Map<Integer, QProfile> byIds : other.profilesByLanguage.values()) {
+      this.addAll(byIds.values());
+    }
+    return this;
+  }
+
+}
index 1db4a9a50541e8d4ec4416ab0b250f948121dea4..c2fb81ba9825fa49fde0f9ad8599ea569c0e5c78 100644 (file)
@@ -28,7 +28,12 @@ import org.sonar.api.batch.rule.CheckFactory;
 import org.sonar.api.platform.ComponentContainer;
 import org.sonar.api.resources.Project;
 import org.sonar.api.scan.filesystem.FileExclusions;
-import org.sonar.batch.*;
+import org.sonar.batch.DefaultProjectClasspath;
+import org.sonar.batch.DefaultSensorContext;
+import org.sonar.batch.DefaultTimeMachine;
+import org.sonar.batch.ProjectTree;
+import org.sonar.batch.ResourceFilters;
+import org.sonar.batch.ViolationFilters;
 import org.sonar.batch.bootstrap.BatchExtensionDictionnary;
 import org.sonar.batch.bootstrap.ExtensionInstaller;
 import org.sonar.batch.bootstrap.ExtensionMatcher;
@@ -52,8 +57,25 @@ import org.sonar.batch.phases.PhasesTimeProfiler;
 import org.sonar.batch.qualitygate.GenerateQualityGateEvents;
 import org.sonar.batch.qualitygate.QualityGateProvider;
 import org.sonar.batch.qualitygate.QualityGateVerifier;
-import org.sonar.batch.rule.*;
-import org.sonar.batch.scan.filesystem.*;
+import org.sonar.batch.rule.ActiveRulesProvider;
+import org.sonar.batch.rule.ModuleQProfiles;
+import org.sonar.batch.rule.QProfileDecorator;
+import org.sonar.batch.rule.QProfileSensor;
+import org.sonar.batch.rule.QProfileVerifier;
+import org.sonar.batch.rule.RulesProfileProvider;
+import org.sonar.batch.scan.filesystem.ComponentIndexer;
+import org.sonar.batch.scan.filesystem.DefaultModuleFileSystem;
+import org.sonar.batch.scan.filesystem.DeprecatedFileFilters;
+import org.sonar.batch.scan.filesystem.ExclusionFilters;
+import org.sonar.batch.scan.filesystem.FileIndexer;
+import org.sonar.batch.scan.filesystem.FileSystemLogger;
+import org.sonar.batch.scan.filesystem.InputFileBuilderFactory;
+import org.sonar.batch.scan.filesystem.LanguageDetectionFactory;
+import org.sonar.batch.scan.filesystem.ModuleFileSystemInitializer;
+import org.sonar.batch.scan.filesystem.ModuleInputFileCache;
+import org.sonar.batch.scan.filesystem.PreviousFileHashLoader;
+import org.sonar.batch.scan.filesystem.ProjectFileSystemAdapter;
+import org.sonar.batch.scan.filesystem.StatusDetectionFactory;
 import org.sonar.batch.scan.report.JsonReport;
 import org.sonar.core.component.ScanPerspectives;
 import org.sonar.core.measure.MeasurementFilters;
@@ -134,6 +156,7 @@ public class ModuleScanContainer extends ComponentContainer {
       new ActiveRulesProvider(),
       new RulesProfileProvider(),
       QProfileSensor.class,
+      QProfileDecorator.class,
       CheckFactory.class,
 
       // report
diff --git a/sonar-batch/src/test/java/org/sonar/batch/rule/QProfileDecoratorTest.java b/sonar-batch/src/test/java/org/sonar/batch/rule/QProfileDecoratorTest.java
new file mode 100644 (file)
index 0000000..30c8023
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * 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.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Scopes;
+import org.sonar.api.test.IsMeasure;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class QProfileDecoratorTest {
+
+  Project project = mock(Project.class);
+  Project moduleA = mock(Project.class);
+  Project moduleB = mock(Project.class);
+  DecoratorContext decoratorContext = mock(DecoratorContext.class);
+
+  @Test
+  public void don_t_run_on_leaf() throws Exception {
+    QProfileDecorator decorator = new QProfileDecorator();
+    when(project.getModules()).thenReturn(Collections.<Project>emptyList());
+    assertThat(decorator.shouldExecuteOnProject(project)).isFalse();
+
+    when(project.getModules()).thenReturn(Arrays.asList(moduleA, moduleB));
+    assertThat(decorator.shouldExecuteOnProject(project)).isTrue();
+  }
+
+  @Test
+  public void aggregate() throws Exception {
+    Measure measureModuleA = new Measure(CoreMetrics.PROFILES, "[{\"id\":2,\"name\":\"Java Two\",\"version\":20,\"language\":\"java\"}]");
+    Measure measureModuleB = new Measure(CoreMetrics.PROFILES, "[{\"id\":3,\"name\":\"Php One\",\"version\":30,\"language\":\"php\"}]");
+    when(decoratorContext.getChildrenMeasures(CoreMetrics.PROFILES)).thenReturn(Arrays.asList(measureModuleA, measureModuleB));
+
+    when(project.getScope()).thenReturn(Scopes.PROJECT);
+
+    QProfileDecorator decorator = new QProfileDecorator();
+    decorator.decorate(project, decoratorContext);
+
+    verify(decoratorContext).saveMeasure(
+      argThat(new IsMeasure(CoreMetrics.PROFILES,
+        "[{\"id\":2,\"name\":\"Java Two\",\"version\":20,\"language\":\"java\"},{\"id\":3,\"name\":\"Php One\",\"version\":30,\"language\":\"php\"}]")));
+  }
+
+  @Test
+  public void aggregate_several_profile_same_language() throws Exception {
+    Measure measureModuleA = new Measure(CoreMetrics.PROFILES, "[{\"id\":2,\"name\":\"Java Two\",\"version\":20,\"language\":\"java\"}]");
+    Measure measureModuleB = new Measure(CoreMetrics.PROFILES, "[{\"id\":3,\"name\":\"Java Three\",\"version\":30,\"language\":\"java\"}]");
+    when(decoratorContext.getChildrenMeasures(CoreMetrics.PROFILES)).thenReturn(Arrays.asList(measureModuleA, measureModuleB));
+
+    when(project.getScope()).thenReturn(Scopes.PROJECT);
+
+    QProfileDecorator decorator = new QProfileDecorator();
+    decorator.decorate(project, decoratorContext);
+
+    verify(decoratorContext).saveMeasure(
+      argThat(new IsMeasure(CoreMetrics.PROFILES,
+        "[{\"id\":2,\"name\":\"Java Two\",\"version\":20,\"language\":\"java\"},{\"id\":3,\"name\":\"Java Three\",\"version\":30,\"language\":\"java\"}]")));
+  }
+
+  @Test
+  public void aggregate_several_profile_same_id() throws Exception {
+    Measure measureModuleA = new Measure(CoreMetrics.PROFILES, "[{\"id\":2,\"name\":\"Java Two\",\"version\":20,\"language\":\"java\"}]");
+    Measure measureModuleB = new Measure(CoreMetrics.PROFILES, "[{\"id\":2,\"name\":\"Java Two\",\"version\":30,\"language\":\"java\"}]");
+    when(decoratorContext.getChildrenMeasures(CoreMetrics.PROFILES)).thenReturn(Arrays.asList(measureModuleA, measureModuleB));
+
+    when(project.getScope()).thenReturn(Scopes.PROJECT);
+
+    QProfileDecorator decorator = new QProfileDecorator();
+    decorator.decorate(project, decoratorContext);
+
+    verify(decoratorContext).saveMeasure(
+      argThat(new IsMeasure(CoreMetrics.PROFILES,
+        "[{\"id\":2,\"name\":\"Java Two\",\"version\":30,\"language\":\"java\"}]")));
+  }
+}
index f12d753fb5f4deddae700bca2e55394e6c750887..85f2509d874c1eced670deaf94dc4f34ccc67258 100644 (file)
@@ -31,7 +31,11 @@ import org.sonar.core.qualityprofile.db.QualityProfileDao;
 import java.util.Collections;
 
 import static org.fest.assertions.Assertions.assertThat;
-import static org.mockito.Mockito.*;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
 
 public class QProfileSensorTest extends AbstractDaoTestCase {
 
@@ -76,9 +80,6 @@ public class QProfileSensorTest extends AbstractDaoTestCase {
     sensor.analyse(project, sensorContext);
 
     checkTable("mark_profiles_as_used", "rules_profiles");
-
-    // no measures on multi-language modules
-    verifyZeroInteractions(sensorContext);
   }
 
   @Test
@@ -97,5 +98,26 @@ public class QProfileSensorTest extends AbstractDaoTestCase {
 
     verify(sensorContext).saveMeasure(argThat(new IsMeasure(CoreMetrics.PROFILE, "Java Two")));
     verify(sensorContext).saveMeasure(argThat(new IsMeasure(CoreMetrics.PROFILE_VERSION, 20.0)));
+    verify(sensorContext).saveMeasure(
+      argThat(new IsMeasure(CoreMetrics.PROFILES, "[{\"id\":2,\"name\":\"Java Two\",\"version\":20,\"language\":\"java\"}]")));
+  }
+
+  @Test
+  public void store_measures_on_multi_lang_module() throws Exception {
+    setupData("shared");
+
+    QualityProfileDao dao = new QualityProfileDao(getMyBatis());
+    when(moduleQProfiles.findByLanguage("java")).thenReturn(new ModuleQProfiles.QProfile(dao.selectById(2)));
+    when(moduleQProfiles.findByLanguage("php")).thenReturn(new ModuleQProfiles.QProfile(dao.selectById(3)));
+    when(moduleQProfiles.findByLanguage("abap")).thenReturn(null);
+    fs.addLanguages("java", "php");
+
+    QProfileSensor sensor = new QProfileSensor(moduleQProfiles, fs, dao);
+    assertThat(sensor.shouldExecuteOnProject(project)).isTrue();
+    sensor.analyse(project, sensorContext);
+
+    verify(sensorContext).saveMeasure(
+      argThat(new IsMeasure(CoreMetrics.PROFILES,
+        "[{\"id\":2,\"name\":\"Java Two\",\"version\":20,\"language\":\"java\"},{\"id\":3,\"name\":\"Php One\",\"version\":30,\"language\":\"php\"}]")));
   }
 }
diff --git a/sonar-batch/src/test/java/org/sonar/batch/rule/UsedQProfilesTest.java b/sonar-batch/src/test/java/org/sonar/batch/rule/UsedQProfilesTest.java
new file mode 100644 (file)
index 0000000..b3cab37
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * 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 static org.fest.assertions.Assertions.assertThat;
+
+public class UsedQProfilesTest {
+
+  @Test
+  public void serialization() throws Exception {
+
+    ModuleQProfiles.QProfile java = new ModuleQProfiles.QProfile(1, "Sonar Way", "java", 1);
+    ModuleQProfiles.QProfile php = new ModuleQProfiles.QProfile(2, "Sonar Way", "php", 1);
+
+    UsedQProfiles used = UsedQProfiles.fromProfiles(java, php);
+    assertThat(used.toJSON()).isEqualTo(
+      "[{\"id\":1,\"name\":\"Sonar Way\",\"version\":1,\"language\":\"java\"},{\"id\":2,\"name\":\"Sonar Way\",\"version\":1,\"language\":\"php\"}]");
+  }
+
+  @Test
+  public void deserialization() throws Exception {
+    UsedQProfiles used = UsedQProfiles
+      .fromJSON("[{\"id\":1,\"name\":\"Sonar Way\",\"version\":1,\"language\":\"java\"},{\"id\":2,\"name\":\"Sonar Way\",\"version\":1,\"language\":\"php\"}]");
+
+    assertThat(used.toJSON()).isEqualTo(
+      "[{\"id\":1,\"name\":\"Sonar Way\",\"version\":1,\"language\":\"java\"},{\"id\":2,\"name\":\"Sonar Way\",\"version\":1,\"language\":\"php\"}]");
+  }
+
+  @Test
+  public void merge() throws Exception {
+    UsedQProfiles first = UsedQProfiles
+      .fromJSON("[{\"id\":1,\"name\":\"Sonar Way\",\"version\":1,\"language\":\"java\"}]");
+
+    UsedQProfiles second = UsedQProfiles
+      .fromJSON("[{\"id\":2,\"name\":\"Sonar Way\",\"version\":1,\"language\":\"php\"}]");
+
+    assertThat(first.merge(second).toJSON()).isEqualTo(
+      "[{\"id\":1,\"name\":\"Sonar Way\",\"version\":1,\"language\":\"java\"},{\"id\":2,\"name\":\"Sonar Way\",\"version\":1,\"language\":\"php\"}]");
+  }
+
+  @Test
+  public void merge_no_duplicate_ids() throws Exception {
+    UsedQProfiles first = UsedQProfiles
+      .fromJSON("[{\"id\":1,\"name\":\"Sonar Way\",\"version\":1,\"language\":\"java\"},{\"id\":2,\"name\":\"Sonar Way\",\"version\":2,\"language\":\"php\"}]");
+
+    UsedQProfiles second = UsedQProfiles
+      .fromJSON("[{\"id\":1,\"name\":\"Sonar Way\",\"version\":2,\"language\":\"java\"},{\"id\":2,\"name\":\"Sonar Way\",\"version\":1,\"language\":\"php\"}]");
+
+    assertThat(first.merge(second).toJSON()).isEqualTo(
+      "[{\"id\":1,\"name\":\"Sonar Way\",\"version\":2,\"language\":\"java\"},{\"id\":2,\"name\":\"Sonar Way\",\"version\":2,\"language\":\"php\"}]");
+  }
+
+}
index 1320f51583f9214e0dd821ec05a1de85702ba92e..3e7898358e43674c87196b0eacfdf25325272998 100644 (file)
@@ -1053,6 +1053,7 @@ widget.description.description=Displays general project information.
 widget.description.key=Key
 widget.description.language=Language
 widget.description.profile=Profile
+widget.description.profiles=Profiles
 widget.description.profile_version_x=version {0}
 widget.description.qualitygate=Quality Gate
 widget.description.alerts=Displays a summary of the project's quality gate status.
index 4a5d14473ecc13dc523e69c2685f669fa8dec57e..6399bff81b38205c6ef0e524fd1ebfe9e0fe33bf 100644 (file)
@@ -2216,7 +2216,15 @@ public final class CoreMetrics {
     .setDomain(DOMAIN_GENERAL)
     .create();
 
+  /**
+   * @deprecated since 4.4 doesn't support multi-language. See {@link #PROFILES_KEY}
+   */
+  @Deprecated
   public static final String PROFILE_KEY = "profile";
+  /**
+   * @deprecated since 4.4 doesn't support multi-language. See {@link #PROFILES_KEY}
+   */
+  @Deprecated
   public static final Metric PROFILE = new Metric.Builder(PROFILE_KEY, "Profile", Metric.ValueType.DATA)
     .setDescription("Selected quality profile")
     .setDomain(DOMAIN_GENERAL)
@@ -2224,12 +2232,15 @@ public final class CoreMetrics {
 
   /**
    * @since 2.9
+   * @deprecated since 4.4 doesn't support multi-language. See {@link #PROFILES_KEY}
    */
+  @Deprecated
   public static final String PROFILE_VERSION_KEY = "profile_version";
-
   /**
    * @since 2.9
+   * @deprecated since 4.4 doesn't support multi-language. See {@link #PROFILES_KEY}
    */
+  @Deprecated
   public static final Metric PROFILE_VERSION = new Metric.Builder(PROFILE_VERSION_KEY, "Profile version", Metric.ValueType.INT)
     .setDescription("Selected quality profile version")
     .setQualitative(false)
@@ -2237,6 +2248,21 @@ public final class CoreMetrics {
     .setHidden(true)
     .create();
 
+  /**
+   * @since 4.4
+   */
+  public static final String PROFILES_KEY = "profiles";
+
+  /**
+   * @since 4.4
+   */
+  public static final Metric PROFILES = new Metric.Builder(PROFILES_KEY, "Profiles", Metric.ValueType.DATA)
+    .setDescription("Details of quality profiles used during analysis")
+    .setQualitative(false)
+    .setDomain(DOMAIN_GENERAL)
+    .setHidden(true)
+    .create();
+
   private static final List<Metric> METRICS;
 
   static {
index e004ec258cc46445d7f27d6f9258fdad29d3ab4a..086005be0f83aa8101c1bee2f8d094567a294645 100644 (file)
@@ -32,7 +32,7 @@ public class CoreMetricsTest {
   @Test
   public void read_metrics_from_class_reflection() {
     List<Metric> metrics = CoreMetrics.getMetrics();
-    assertThat(metrics).hasSize(151);
+    assertThat(metrics).hasSize(152);
     assertThat(metrics).contains(CoreMetrics.NCLOC, CoreMetrics.DIRECTORIES);
   }
 
index a58c1498217c0ad00a55d9d19d18359f4762adb1..d228a5eaf519ec57db624711719ad2b313d76f65 100644 (file)
@@ -378,6 +378,7 @@ class Metric < ActiveRecord::Base
   ALERT_STATUS = 'alert_status'
   QUALITY_GATE_DETAILS = 'quality_gate_details'
   PROFILE='profile'
+  PROFILES='profiles'
 
   private