]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6303 WS to compare quality profiles
authorJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Fri, 10 Apr 2015 16:03:35 +0000 (18:03 +0200)
committerJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Wed, 15 Apr 2015 07:49:39 +0000 (09:49 +0200)
server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileComparator.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/QProfileCompareAction.java [new file with mode: 0644]
server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/example-compare.json [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/QProfileCompareActionMediumTest.java [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/qualityprofile/ws/QProfileCompareActionMediumTest/compare_nominal.json [new file with mode: 0644]

index f3b490d007bdc284e917c5fdcb84179e931644f3..95bad79553ee4154688105313680b65bff73c88f 100644 (file)
@@ -377,6 +377,7 @@ class ServerComponents {
     pico.addSingleton(QProfileLookup.class);
     pico.addSingleton(QProfileProjectOperations.class);
     pico.addSingleton(QProfileProjectLookup.class);
+    pico.addSingleton(QProfileComparator.class);
     pico.addSingleton(BuiltInProfiles.class);
     pico.addSingleton(QProfileRestoreBuiltInAction.class);
     pico.addSingleton(QProfileSearchAction.class);
@@ -392,6 +393,7 @@ class ServerComponents {
     pico.addSingleton(QProfileInheritanceAction.class);
     pico.addSingleton(QProfileChangeParentAction.class);
     pico.addSingleton(QProfileChangelogAction.class);
+    pico.addSingleton(QProfileCompareAction.class);
     pico.addSingleton(QProfilesWs.class);
     pico.addSingleton(ProfilesWs.class);
     pico.addSingleton(RuleActivationActions.class);
diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileComparator.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileComparator.java
new file mode 100644 (file)
index 0000000..767dfaa
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.qualityprofile;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.MapDifference;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.sonar.api.ServerComponent;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.qualityprofile.db.QualityProfileDto;
+import org.sonar.core.util.NonNullInputFunction;
+import org.sonar.server.db.DbClient;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+public class QProfileComparator implements ServerComponent {
+
+  private final DbClient dbClient;
+
+  private final QProfileLoader profileLoader;
+
+  public QProfileComparator(DbClient dbClient, QProfileLoader profileLoader) {
+    this.dbClient = dbClient;
+    this.profileLoader = profileLoader;
+  }
+
+  public QProfileComparisonResult compare(String leftKey, String rightKey) {
+    QProfileComparisonResult result = new QProfileComparisonResult();
+
+    DbSession dbSession = dbClient.openSession(false);
+
+    try {
+      compare(leftKey, rightKey, dbSession, result);
+    } finally {
+      dbSession.close();
+    }
+
+    return result;
+  }
+
+  private void compare(String leftKey, String rightKey, DbSession session, QProfileComparisonResult result) {
+    result.left = dbClient.qualityProfileDao().getByKey(session, leftKey);
+    Preconditions.checkArgument(result.left != null, String.format("Could not find left profile '%s'", leftKey));
+    result.right = dbClient.qualityProfileDao().getByKey(session, rightKey);
+    Preconditions.checkArgument(result.right != null, String.format("Could not find right profile '%s'", leftKey));
+
+    Map<RuleKey, ActiveRule> leftActiveRulesByRuleKey = loadActiveRules(leftKey);
+    Map<RuleKey, ActiveRule> rightActiveRulesByRuleKey = loadActiveRules(rightKey);
+
+    Set<RuleKey> allRules = Sets.newHashSet();
+    allRules.addAll(leftActiveRulesByRuleKey.keySet());
+    allRules.addAll(rightActiveRulesByRuleKey.keySet());
+
+    for (RuleKey ruleKey : allRules) {
+      if (!leftActiveRulesByRuleKey.containsKey(ruleKey)) {
+        result.inRight.put(ruleKey, rightActiveRulesByRuleKey.get(ruleKey));
+      } else if (!rightActiveRulesByRuleKey.containsKey(ruleKey)) {
+        result.inLeft.put(ruleKey, leftActiveRulesByRuleKey.get(ruleKey));
+      } else {
+        compare(leftActiveRulesByRuleKey.get(ruleKey), rightActiveRulesByRuleKey.get(ruleKey), result);
+      }
+    }
+  }
+
+  private void compare(ActiveRule leftRule, ActiveRule rightRule, QProfileComparisonResult result) {
+    RuleKey key = leftRule.key().ruleKey();
+    if (leftRule.params().equals(rightRule.params()) && leftRule.severity().equals(rightRule.severity())) {
+      result.same.put(key, leftRule);
+    } else {
+      ActiveRuleDiff diff = new ActiveRuleDiff();
+      diff.key = key;
+
+      if (leftRule.severity().equals(rightRule.severity())) {
+        diff.leftSeverity = diff.rightSeverity = leftRule.severity();
+      } else {
+        diff.leftSeverity = leftRule.severity();
+        diff.rightSeverity = rightRule.severity();
+      }
+
+      diff.paramDifference = Maps.difference(leftRule.params(), rightRule.params());
+      result.modified.put(key, diff);
+    }
+  }
+
+  private Map<RuleKey, ActiveRule> loadActiveRules(String profileKey) {
+    return Maps.uniqueIndex(profileLoader.findActiveRulesByProfile(profileKey), new NonNullInputFunction<ActiveRule, RuleKey>() {
+      @Override
+      protected RuleKey doApply(ActiveRule input) {
+        return input.key().ruleKey();
+      }
+    });
+  }
+
+  public static class QProfileComparisonResult {
+
+    private QualityProfileDto left;
+    private QualityProfileDto right;
+    private Map<RuleKey, ActiveRule> inLeft = Maps.newHashMap();
+    private Map<RuleKey, ActiveRule> inRight = Maps.newHashMap();
+    private Map<RuleKey, ActiveRuleDiff> modified = Maps.newHashMap();
+    private Map<RuleKey, ActiveRule> same = Maps.newHashMap();
+
+    public QualityProfileDto left() {
+      return left;
+    }
+
+    public QualityProfileDto right() {
+      return right;
+    }
+
+    public Map<RuleKey, ActiveRule> inLeft() {
+      return inLeft;
+    }
+
+    public Map<RuleKey, ActiveRule> inRight() {
+      return inRight;
+    }
+
+    public Map<RuleKey, ActiveRuleDiff> modified() {
+      return modified;
+    }
+
+    public Map<RuleKey, ActiveRule> same() {
+      return same;
+    }
+
+    public Collection<RuleKey> collectRuleKeys() {
+      Set<RuleKey> keys = Sets.newHashSet();
+      keys.addAll(inLeft.keySet());
+      keys.addAll(inRight.keySet());
+      keys.addAll(modified.keySet());
+      keys.addAll(same.keySet());
+      return keys;
+    }
+  }
+
+  public static class ActiveRuleDiff {
+    private RuleKey key;
+    private String leftSeverity;
+    private String rightSeverity;
+    private MapDifference<String, String> paramDifference;
+
+    public RuleKey key() {
+      return key;
+    }
+
+    public String leftSeverity() {
+      return leftSeverity;
+    }
+
+    public String rightSeverity() {
+      return rightSeverity;
+    }
+
+    public MapDifference<String, String> paramDifference() {
+      return paramDifference;
+    }
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/QProfileCompareAction.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/QProfileCompareAction.java
new file mode 100644 (file)
index 0000000..754095e
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.qualityprofile.ws;
+
+import com.google.common.collect.MapDifference.ValueDifference;
+import com.google.common.collect.Maps;
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService.NewAction;
+import org.sonar.api.server.ws.WebService.NewController;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.core.qualityprofile.db.QualityProfileDto;
+import org.sonar.core.util.NonNullInputFunction;
+import org.sonar.server.qualityprofile.ActiveRule;
+import org.sonar.server.qualityprofile.QProfileComparator;
+import org.sonar.server.qualityprofile.QProfileComparator.ActiveRuleDiff;
+import org.sonar.server.qualityprofile.QProfileComparator.QProfileComparisonResult;
+import org.sonar.server.rule.Rule;
+import org.sonar.server.rule.RuleRepositories;
+import org.sonar.server.rule.RuleRepositories.Repository;
+import org.sonar.server.rule.RuleService;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+public class QProfileCompareAction implements BaseQProfileWsAction {
+
+  private static final String PARAM_LEFT_KEY = "leftKey";
+  private static final String PARAM_RIGHT_KEY = "rightKey";
+
+  private final QProfileComparator comparator;
+  private final RuleService ruleService;
+  private final RuleRepositories ruleRepositories;
+  private final Languages languages;
+
+  public QProfileCompareAction(QProfileComparator comparator, RuleService ruleService, RuleRepositories ruleRepositories, Languages languages) {
+    this.comparator = comparator;
+    this.ruleService = ruleService;
+    this.ruleRepositories = ruleRepositories;
+    this.languages = languages;
+  }
+
+  @Override
+  public void define(NewController context) {
+    NewAction compare = context.createAction("compare")
+      .setDescription("Compare two quality profiles.")
+      .setHandler(this)
+      .setResponseExample(getClass().getResource("example-compare.json"))
+      .setSince("5.2");
+
+    compare.createParam(PARAM_LEFT_KEY)
+      .setDescription("A profile key.")
+      .setExampleValue("java-sonar-way-12345")
+      .setRequired(true);
+
+    compare.createParam(PARAM_RIGHT_KEY)
+      .setDescription("Another profile key.")
+      .setExampleValue("java-sonar-way-with-findbugs-23456")
+      .setRequired(true);
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    String leftKey = request.mandatoryParam(PARAM_LEFT_KEY);
+    String rightKey = request.mandatoryParam(PARAM_RIGHT_KEY);
+
+    QProfileComparisonResult result = comparator.compare(leftKey, rightKey);
+
+    List<Rule> referencedRules = ruleService.getByKeys(result.collectRuleKeys());
+    Map<RuleKey, Rule> rulesByKey = Maps.uniqueIndex(referencedRules, new NonNullInputFunction<Rule, RuleKey>() {
+      @Override
+      protected RuleKey doApply(Rule input) {
+        return input.key();
+      }
+    });
+
+    writeResult(response.newJsonWriter(), result, rulesByKey);
+  }
+
+  private void writeResult(JsonWriter json, QProfileComparisonResult result, Map<RuleKey, Rule> rulesByKey) {
+    json.beginObject();
+
+    json.name("left").beginObject();
+    writeProfile(json, result.left());
+    json.endObject();
+
+    json.name("right").beginObject();
+    writeProfile(json, result.right());
+    json.endObject();
+
+    json.name("inLeft");
+    writeRules(json, result.inLeft(), rulesByKey);
+
+    json.name("inRight");
+    writeRules(json, result.inRight(), rulesByKey);
+
+    json.name("modified");
+    writeDifferences(json, result.modified(), rulesByKey);
+
+    json.name("same");
+    writeRules(json, result.same(), rulesByKey);
+
+    json.endObject().close();
+  }
+
+  private void writeProfile(JsonWriter json, QualityProfileDto profile) {
+    json.prop("key", profile.getKey())
+      .prop("name", profile.getName());
+  }
+
+  private void writeRules(JsonWriter json, Map<RuleKey, ActiveRule> activeRules, Map<RuleKey, Rule> rulesByKey) {
+    json.beginArray();
+    for (Entry<RuleKey, ActiveRule> activeRule : activeRules.entrySet()) {
+      RuleKey key = activeRule.getKey();
+      ActiveRule value = activeRule.getValue();
+
+      json.beginObject();
+      writeRule(json, key, rulesByKey);
+      json.prop("severity", value.severity());
+      json.endObject();
+    }
+    json.endArray();
+  }
+
+  private void writeRule(JsonWriter json, RuleKey key, Map<RuleKey, Rule> rulesByKey) {
+    String repositoryKey = key.repository();
+    json.prop("key", key.toString())
+      .prop("name", rulesByKey.get(key).name())
+      .prop("pluginKey", repositoryKey);
+
+    Repository repo = ruleRepositories.repository(repositoryKey);
+    if (repo != null) {
+      String languageKey = repo.getLanguage();
+      Language language = languages.get(languageKey);
+
+      json.prop("pluginName", repo.getName());
+      json.prop("languageKey", languageKey);
+      json.prop("languageName", language == null ? null : language.getName());
+    }
+  }
+
+  private void writeDifferences(JsonWriter json, Map<RuleKey, ActiveRuleDiff> modified, Map<RuleKey, Rule> rulesByKey) {
+    json.beginArray();
+    for (Entry<RuleKey, ActiveRuleDiff> diffEntry : modified.entrySet()) {
+      RuleKey key = diffEntry.getKey();
+      ActiveRuleDiff value = diffEntry.getValue();
+      json.beginObject();
+      writeRule(json, key, rulesByKey);
+
+      json.name("left").beginObject();
+      json.prop("severity", value.leftSeverity());
+      json.name("params").beginObject();
+      for (Entry<String, ValueDifference<String>> valueDiff : value.paramDifference().entriesDiffering().entrySet()) {
+        json.prop(valueDiff.getKey(), valueDiff.getValue().leftValue());
+      }
+      for (Entry<String, String> valueDiff : value.paramDifference().entriesOnlyOnLeft().entrySet()) {
+        json.prop(valueDiff.getKey(), valueDiff.getValue());
+      }
+      // params
+      json.endObject();
+      // left
+      json.endObject();
+
+      json.name("right").beginObject();
+      json.prop("severity", value.rightSeverity());
+      json.name("params").beginObject();
+      for (Entry<String, ValueDifference<String>> valueDiff : value.paramDifference().entriesDiffering().entrySet()) {
+        json.prop(valueDiff.getKey(), valueDiff.getValue().rightValue());
+      }
+      for (Entry<String, String> valueDiff : value.paramDifference().entriesOnlyOnRight().entrySet()) {
+        json.prop(valueDiff.getKey(), valueDiff.getValue());
+      }
+      // params
+      json.endObject();
+      // right
+      json.endObject();
+
+      // rule
+      json.endObject();
+    }
+    json.endArray();
+  }
+
+}
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/example-compare.json b/server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/example-compare.json
new file mode 100644 (file)
index 0000000..396a06c
--- /dev/null
@@ -0,0 +1,65 @@
+{
+   "left" : {
+      "key" : "js-my-profile-19170",
+      "name" : "My Profile"
+   },
+   "right" : {
+      "key" : "js-my-other-profile-12367",
+      "name" : "My Other Profile"
+   },
+   "same" : [
+      {
+         "key" : "javascript:EqEqEq",
+         "pluginKey" : "javascript",
+         "pluginName" : "SonarQube",
+         "languageKey": "js",
+         "languageName": "JavaScript",
+         "name" : "\"===\" and \"!==\" should be used instead of \"==\" and \"!=\"",
+         "severity" : "MAJOR"
+      }
+   ],
+   "inLeft" : [
+      {
+         "key" : "javascript:TrailingWhitespace",
+         "pluginKey" : "javascript",
+         "pluginName" : "SonarQube",
+         "languageKey": "js",
+         "languageName": "JavaScript",
+         "name" : "Avoid trailing whitespaces",
+         "severity" : "MAJOR"
+      }
+   ],
+   "inRight" : [
+      {
+         "key" : "javascript:TabCharacter",
+         "pluginKey" : "javascript",
+         "pluginName" : "SonarQube",
+         "languageKey": "js",
+         "languageName": "JavaScript",
+         "name" : "Avoid use of tabulation character",
+         "severity" : "MINOR"
+      }
+   ],
+   "modified" : [
+      {
+         "key" : "javascript:ExcessiveParameterList",
+         "pluginKey" : "javascript",
+         "pluginName" : "SonarQube",
+         "languageKey": "js",
+         "languageName": "JavaScript",
+         "name" : "Avoid function with too many parameters",
+         "right" : {
+            "severity" : "MAJOR",
+            "params" : {
+               "maximumFunctionParameters" : "7"
+            }
+         },
+         "left" : {
+            "severity" : "MAJOR",
+            "params" : {
+               "maximumFunctionParameters" : "10"
+            }
+         }
+      }
+   ]
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/QProfileCompareActionMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/QProfileCompareActionMediumTest.java
new file mode 100644 (file)
index 0000000..9abcb7a
--- /dev/null
@@ -0,0 +1,197 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.qualityprofile.ws;
+
+import org.apache.commons.lang.StringUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.server.rule.RuleParamType;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.qualityprofile.db.ActiveRuleDto;
+import org.sonar.core.qualityprofile.db.ActiveRuleParamDto;
+import org.sonar.core.qualityprofile.db.QualityProfileDto;
+import org.sonar.core.rule.RuleDto;
+import org.sonar.core.rule.RuleParamDto;
+import org.sonar.server.db.DbClient;
+import org.sonar.server.qualityprofile.QProfileName;
+import org.sonar.server.qualityprofile.QProfileTesting;
+import org.sonar.server.tester.ServerTester;
+import org.sonar.server.ws.WsTester;
+
+public class QProfileCompareActionMediumTest {
+
+  @ClassRule
+  public static ServerTester tester = new ServerTester().addXoo()
+    .addComponents(new RulesDefinition() {
+      @Override
+      public void define(Context context) {
+        context.createRepository("blah", "xoo")
+          .setName("Blah")
+          .done();
+      }
+    });
+
+  private DbClient db;
+
+  private WsTester wsTester;
+
+  private DbSession session;
+
+  @Before
+  public void setUp() throws Exception {
+    tester.clearDbAndIndexes();
+    db = tester.get(DbClient.class);
+    session = db.openSession(false);
+
+    wsTester = new WsTester(tester.get(QProfilesWs.class));
+  }
+
+  @After
+  public void tearDown() {
+    session.close();
+  }
+
+  @Test
+  public void compare_nominal() throws Exception {
+    RuleDto rule1 = createRule("xoo", "rule1");
+    RuleDto rule2 = createRule("xoo", "rule2");
+    RuleDto rule3 = createRule("xoo", "rule3");
+    RuleDto rule4 = createRuleWithParam("xoo", "rule4");
+    RuleDto rule5 = createRule("xoo", "rule5");
+
+    /*
+     * Profile 1:
+     * - rule 1 active (on both profiles) => "same"
+     * - rule 2 active (only in this profile) => "inLeft"
+     * - rule 4 active with different parameters => "modified"
+     * - rule 5 active with different severity => "modified"
+     */
+    QualityProfileDto profile1 = createProfile("xoo", "Profile 1", "xoo-profile-1-01234");
+    createActiveRule(rule1, profile1);
+    createActiveRule(rule2, profile1);
+    createActiveRuleWithParam(rule4, profile1, "polop");
+    createActiveRuleWithSeverity(rule5, profile1, Severity.MINOR);
+    session.commit();
+
+    /*
+     * Profile 1:
+     * - rule 1 active (on both profiles) => "same"
+     * - rule 3 active (only in this profile) => "inRight"
+     * - rule 4 active with different parameters => "modified"
+     */
+    QualityProfileDto profile2 = createProfile("xoo", "Profile 2", "xoo-profile-2-12345");
+    createActiveRule(rule1, profile2);
+    createActiveRule(rule3, profile2);
+    createActiveRuleWithParam(rule4, profile2, "palap");
+    createActiveRuleWithSeverity(rule5, profile2, Severity.MAJOR);
+    session.commit();
+
+    wsTester.newGetRequest("api/qualityprofiles", "compare")
+      .setParam("leftKey", profile1.getKey())
+      .setParam("rightKey", profile2.getKey())
+      .execute().assertJson(this.getClass(), "compare_nominal.json");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void fail_on_missing_left_param() throws Exception {
+    wsTester.newGetRequest("api/qualityprofiles", "compare")
+      .setParam("rightKey", "polop")
+      .execute();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void fail_on_missing_right_param() throws Exception {
+    wsTester.newGetRequest("api/qualityprofiles", "compare")
+      .setParam("leftKey", "polop")
+      .execute();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void fail_on_left_profile_not_found() throws Exception {
+    createProfile("xoo", "Right", "xoo-right-12345");
+    wsTester.newGetRequest("api/qualityprofiles", "compare")
+      .setParam("leftKey", "polop")
+      .setParam("rightKey", "xoo-right-12345")
+      .execute();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void fail_on_right_profile_not_found() throws Exception {
+    createProfile("xoo", "Left", "xoo-left-12345");
+    wsTester.newGetRequest("api/qualityprofiles", "compare")
+      .setParam("leftKey", "xoo-left-12345")
+      .setParam("rightKey", "polop")
+      .execute();
+  }
+
+  private QualityProfileDto createProfile(String lang, String name, String key) {
+    QualityProfileDto profile = QProfileTesting.newDto(new QProfileName(lang, name), key);
+    db.qualityProfileDao().insert(session, profile);
+    session.commit();
+    return profile;
+  }
+
+  private RuleDto createRule(String lang, String id) {
+    RuleDto rule = RuleDto.createFor(RuleKey.of("blah", id))
+      .setName(StringUtils.capitalize(id))
+      .setLanguage(lang)
+      .setSeverity(Severity.BLOCKER)
+      .setStatus(RuleStatus.READY);
+    db.ruleDao().insert(session, rule);
+    RuleParamDto param = RuleParamDto.createFor(rule).setName("param_" + id).setType(RuleParamType.STRING.toString());
+    db.ruleDao().addRuleParam(session, rule, param);
+    return rule;
+  }
+
+  private RuleDto createRuleWithParam(String lang, String id) {
+    RuleDto rule = createRule(lang, id);
+    RuleParamDto param = RuleParamDto.createFor(rule).setName("param_" + id).setType(RuleParamType.STRING.toString());
+    db.ruleDao().addRuleParam(session, rule, param);
+    return rule;
+  }
+
+  private ActiveRuleDto createActiveRule(RuleDto rule, QualityProfileDto profile) {
+    ActiveRuleDto activeRule = ActiveRuleDto.createFor(profile, rule)
+      .setSeverity(rule.getSeverityString());
+    db.activeRuleDao().insert(session, activeRule);
+    return activeRule;
+  }
+
+  private ActiveRuleDto createActiveRuleWithParam(RuleDto rule, QualityProfileDto profile, String value) {
+    ActiveRuleDto activeRule = createActiveRule(rule, profile);
+    RuleParamDto paramDto = db.ruleDao().findRuleParamsByRuleKey(session, rule.getKey()).get(0);
+    ActiveRuleParamDto activeRuleParam = ActiveRuleParamDto.createFor(paramDto).setValue(value);
+    db.activeRuleDao().addParam(session, activeRule, activeRuleParam);
+    return activeRule;
+  }
+
+  private ActiveRuleDto createActiveRuleWithSeverity(RuleDto rule, QualityProfileDto profile, String severity) {
+    ActiveRuleDto activeRule = ActiveRuleDto.createFor(profile, rule)
+      .setSeverity(severity);
+    db.activeRuleDao().insert(session, activeRule);
+    return activeRule;
+  }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/qualityprofile/ws/QProfileCompareActionMediumTest/compare_nominal.json b/server/sonar-server/src/test/resources/org/sonar/server/qualityprofile/ws/QProfileCompareActionMediumTest/compare_nominal.json
new file mode 100644 (file)
index 0000000..1d0ef29
--- /dev/null
@@ -0,0 +1,79 @@
+{
+   "left" : {
+      "key" : "xoo-profile-1-01234",
+      "name" : "Profile 1"
+   },
+   "right" : {
+      "key" : "xoo-profile-2-12345",
+      "name" : "Profile 2"
+   },
+   "same" : [
+      {
+         "key" : "blah:rule1",
+         "pluginKey" : "blah",
+         "pluginName" : "Blah",
+         "languageKey": "xoo",
+         "languageName": "Xoo",
+         "name" : "Rule1",
+         "severity" : "BLOCKER"
+      }
+   ],
+   "inLeft" : [
+      {
+         "key" : "blah:rule2",
+         "pluginKey" : "blah",
+         "pluginName" : "Blah",
+         "languageKey": "xoo",
+         "languageName": "Xoo",
+         "name" : "Rule2",
+         "severity" : "BLOCKER"
+      }
+   ],
+   "inRight" : [
+      {
+         "key" : "blah:rule3",
+         "pluginKey" : "blah",
+         "pluginName" : "Blah",
+         "languageKey": "xoo",
+         "languageName": "Xoo",
+         "name" : "Rule3",
+         "severity" : "BLOCKER"
+      }
+   ],
+   "modified" : [
+      {
+         "key" : "blah:rule4",
+         "pluginKey" : "blah",
+         "pluginName" : "Blah",
+         "languageKey": "xoo",
+         "languageName": "Xoo",
+         "name" : "Rule4",
+         "left" : {
+            "severity" : "BLOCKER",
+            "params" : {
+               "param_rule4" : "polop"
+            }
+         },
+         "right" : {
+            "severity" : "BLOCKER",
+            "params" : {
+               "param_rule4" : "palap"
+            }
+         }
+      },
+      {
+         "key" : "blah:rule5",
+         "pluginKey" : "blah",
+         "pluginName" : "Blah",
+         "languageKey": "xoo",
+         "languageName": "Xoo",
+         "name" : "Rule5",
+         "left" : {
+            "severity" : "MINOR"
+         },
+         "right" : {
+            "severity" : "MAJOR"
+         }
+      }
+   ]
+}