]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7009 Add issue exclusions in compute engine
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Mon, 16 May 2016 16:08:30 +0000 (18:08 +0200)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 18 May 2016 12:29:40 +0000 (14:29 +0200)
server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java
server/sonar-server/src/main/java/org/sonar/server/computation/issue/TrackerRawInputFactory.java
server/sonar-server/src/main/java/org/sonar/server/computation/issue/filter/IssueFilter.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/issue/filter/IssuePattern.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/issue/filter/package-info.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/issue/IntegrateIssuesVisitorTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/issue/TrackerRawInputFactoryTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/issue/filter/IssueFilterTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/issue/filter/IssuePatternTest.java [new file with mode: 0644]

index e78c44c8e79169e85d567f665be86861b2e1b5a3..b0e9666e7efa45decf79e3a9c5e8945cb079e64f 100644 (file)
@@ -70,6 +70,7 @@ import org.sonar.server.computation.issue.commonrule.DuplicatedBlockRule;
 import org.sonar.server.computation.issue.commonrule.LineCoverageRule;
 import org.sonar.server.computation.issue.commonrule.SkippedTestRule;
 import org.sonar.server.computation.issue.commonrule.TestErrorRule;
+import org.sonar.server.computation.issue.filter.IssueFilter;
 import org.sonar.server.computation.language.LanguageRepositoryImpl;
 import org.sonar.server.computation.measure.MeasureComputersHolderImpl;
 import org.sonar.server.computation.measure.MeasureComputersVisitor;
@@ -172,6 +173,7 @@ public final class ReportComputeEngineContainerPopulator implements ContainerPop
       IssueLifecycle.class,
       ComponentsWithUnprocessedIssues.class,
       ComponentIssuesRepositoryImpl.class,
+      IssueFilter.class,
 
       // common rules
       CommonRuleEngineImpl.class,
index 75cdfe284afd874bd176e4fbda002cae11003914..d692dfd0cd3281a403396727f6572e405fdb3749 100644 (file)
@@ -38,6 +38,7 @@ import org.sonar.server.computation.batch.BatchReportReader;
 import org.sonar.server.computation.component.Component;
 import org.sonar.server.computation.component.TreeRootHolder;
 import org.sonar.server.computation.issue.commonrule.CommonRuleEngine;
+import org.sonar.server.computation.issue.filter.IssueFilter;
 import org.sonar.server.computation.source.SourceLinesRepository;
 import org.sonar.server.rule.CommonRuleKeys;
 
@@ -50,13 +51,15 @@ public class TrackerRawInputFactory {
   private final BatchReportReader reportReader;
   private final SourceLinesRepository sourceLinesRepository;
   private final CommonRuleEngine commonRuleEngine;
+  private final IssueFilter issueFilter;
 
   public TrackerRawInputFactory(TreeRootHolder treeRootHolder, BatchReportReader reportReader,
-    SourceLinesRepository sourceLinesRepository, CommonRuleEngine commonRuleEngine) {
+    SourceLinesRepository sourceLinesRepository, CommonRuleEngine commonRuleEngine, IssueFilter issueFilter) {
     this.treeRootHolder = treeRootHolder;
     this.reportReader = reportReader;
     this.sourceLinesRepository = sourceLinesRepository;
     this.commonRuleEngine = commonRuleEngine;
+    this.issueFilter = issueFilter;
   }
 
   public Input<DefaultIssue> create(Component component) {
@@ -86,7 +89,9 @@ public class TrackerRawInputFactory {
       List<DefaultIssue> result = new ArrayList<>();
 
       for (DefaultIssue commonRuleIssue : commonRuleEngine.process(component)) {
-        result.add(init(commonRuleIssue));
+        if (issueFilter.accept(commonRuleIssue, component)) {
+          result.add(init(commonRuleIssue));
+        }
       }
       try (CloseableIterator<ScannerReport.Issue> reportIssues = reportReader.readComponentIssues(component.getReportAttributes().getRef())) {
         // optimization - do not load line hashes if there are no issues -> getLineHashSequence() is executed
@@ -95,7 +100,9 @@ public class TrackerRawInputFactory {
           ScannerReport.Issue reportIssue = reportIssues.next();
           if (isIssueOnUnsupportedCommonRule(reportIssue)) {
             DefaultIssue issue = toIssue(getLineHashSequence(), reportIssue);
-            result.add(issue);
+            if (issueFilter.accept(issue, component)) {
+              result.add(issue);
+            }
           } else {
             Loggers.get(getClass()).debug("Ignored issue from analysis report on rule {}:{}", reportIssue.getRuleRepository(), reportIssue.getRuleKey());
           }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/issue/filter/IssueFilter.java b/server/sonar-server/src/main/java/org/sonar/server/computation/issue/filter/IssueFilter.java
new file mode 100644 (file)
index 0000000..3823223
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.issue.filter;
+
+import com.google.common.base.Splitter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.SettingsRepository;
+import org.sonar.server.computation.component.TreeRootHolder;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static java.lang.String.format;
+import static org.apache.commons.lang.StringUtils.defaultIfBlank;
+import static org.sonar.core.config.IssueExclusionProperties.PATTERNS_MULTICRITERIA_EXCLUSION_KEY;
+import static org.sonar.core.config.IssueExclusionProperties.PATTERNS_MULTICRITERIA_INCLUSION_KEY;
+import static org.sonar.core.config.IssueExclusionProperties.RESOURCE_KEY;
+import static org.sonar.core.config.IssueExclusionProperties.RULE_KEY;
+import static org.sonar.server.computation.component.Component.Type.FILE;
+
+@ComputeEngineSide
+public class IssueFilter {
+
+  private static final Logger LOG = Loggers.get(IssueFilter.class);
+
+  private final List<IssuePattern> exclusionPatterns;
+  private final List<IssuePattern> inclusionPatterns;
+
+  public IssueFilter(TreeRootHolder treeRootHolder, SettingsRepository settingsRepository) {
+    Settings settings = settingsRepository.getSettings(treeRootHolder.getRoot());
+    this.exclusionPatterns = loadPatterns(PATTERNS_MULTICRITERIA_EXCLUSION_KEY, settings);
+    this.inclusionPatterns = loadPatterns(PATTERNS_MULTICRITERIA_INCLUSION_KEY, settings);
+  }
+
+  public boolean accept(DefaultIssue issue, Component component) {
+    if (component.getType() != FILE || (exclusionPatterns.isEmpty() && inclusionPatterns.isEmpty())) {
+      return true;
+    }
+    if (isExclude(issue, component)) {
+      return false;
+    }
+    return isInclude(issue, component);
+  }
+
+  private boolean isExclude(DefaultIssue issue, Component component) {
+    IssuePattern matchingPattern = null;
+    Iterator<IssuePattern> patternIterator = exclusionPatterns.iterator();
+    while (matchingPattern == null && patternIterator.hasNext()) {
+      IssuePattern nextPattern = patternIterator.next();
+      if (nextPattern.match(issue, component)) {
+        matchingPattern = nextPattern;
+      }
+    }
+    if (matchingPattern != null) {
+      LOG.debug("Issue {} ignored by exclusion pattern {}", issue, matchingPattern);
+      return true;
+    }
+    return false;
+  }
+
+  private boolean isInclude(DefaultIssue issue, Component component) {
+    boolean atLeastOneRuleMatched = false;
+    boolean atLeastOnePatternFullyMatched = false;
+    IssuePattern matchingPattern = null;
+
+    for (IssuePattern pattern : inclusionPatterns) {
+      if (pattern.getRulePattern().match(issue.ruleKey().toString())) {
+        atLeastOneRuleMatched = true;
+        String componentPath = component.getReportAttributes().getPath();
+        if (componentPath != null && pattern.getComponentPattern().match(componentPath)) {
+          atLeastOnePatternFullyMatched = true;
+          matchingPattern = pattern;
+        }
+      }
+    }
+
+    if (atLeastOneRuleMatched) {
+      if (atLeastOnePatternFullyMatched) {
+        LOG.debug("Issue {} enforced by pattern {}", issue, matchingPattern);
+      }
+      return atLeastOnePatternFullyMatched;
+    } else {
+      return true;
+    }
+  }
+
+  private static List<IssuePattern> loadPatterns(String propertyKey, Settings settings) {
+    List<IssuePattern> patterns = new ArrayList<>();
+    String patternConf = defaultIfBlank(settings.getString(propertyKey), "");
+    for (String id : Splitter.on(",").omitEmptyStrings().split(patternConf)) {
+      String propPrefix = propertyKey + "." + id + ".";
+      String componentPathPattern = settings.getString(propPrefix + RESOURCE_KEY);
+      checkArgument(!isNullOrEmpty(componentPathPattern), format("File path pattern cannot be empty. Please check '%s' settings", propertyKey));
+      String ruleKeyPattern = settings.getString(propPrefix + RULE_KEY);
+      checkArgument(!isNullOrEmpty(ruleKeyPattern), format("Rule key pattern cannot be empty. Please check '%s' settings", propertyKey));
+      patterns.add(new IssuePattern(componentPathPattern, ruleKeyPattern));
+    }
+    return patterns;
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/issue/filter/IssuePattern.java b/server/sonar-server/src/main/java/org/sonar/server/computation/issue/filter/IssuePattern.java
new file mode 100644 (file)
index 0000000..86d7b42
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.issue.filter;
+
+import javax.annotation.Nullable;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.utils.WildcardPattern;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.server.computation.component.Component;
+
+public class IssuePattern {
+
+  private WildcardPattern componentPattern;
+  private WildcardPattern rulePattern;
+
+  public IssuePattern(String componentPattern, String rulePattern) {
+    this.componentPattern = WildcardPattern.create(componentPattern);
+    this.rulePattern = WildcardPattern.create(rulePattern);
+  }
+
+  public WildcardPattern getComponentPattern() {
+    return componentPattern;
+  }
+
+  public WildcardPattern getRulePattern() {
+    return rulePattern;
+  }
+
+  boolean match(DefaultIssue issue, Component component) {
+    return matchComponent(component.getReportAttributes().getPath()) && matchRule(issue.ruleKey());
+  }
+
+  boolean matchRule(RuleKey rule) {
+    return rulePattern.match(rule.toString());
+  }
+
+  boolean matchComponent(@Nullable String path) {
+    return path != null && componentPattern.match(path);
+  }
+
+  @Override
+  public String toString() {
+    return "IssuePattern{" +
+      "componentPattern=" + componentPattern +
+      ", rulePattern=" + rulePattern +
+      '}';
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/issue/filter/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/computation/issue/filter/package-info.java
new file mode 100644 (file)
index 0000000..08a42d6
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.computation.issue.filter;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index 8e4f582da5254488ee6679b0221303432e2929a9..ea94c07be317a9310e49082f814d6a34e14b922c 100644 (file)
@@ -45,6 +45,7 @@ import org.sonar.server.computation.batch.TreeRootHolderRule;
 import org.sonar.server.computation.component.Component;
 import org.sonar.server.computation.component.TypeAwareVisitor;
 import org.sonar.server.computation.issue.commonrule.CommonRuleEngineImpl;
+import org.sonar.server.computation.issue.filter.IssueFilter;
 import org.sonar.server.computation.qualityprofile.ActiveRulesHolderRule;
 import org.sonar.server.computation.source.SourceLinesRepositoryRule;
 import org.sonar.server.issue.IssueTesting;
@@ -53,9 +54,11 @@ import static com.google.common.collect.Lists.newArrayList;
 import static com.google.common.collect.Sets.newHashSet;
 import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 import static org.sonar.server.computation.component.ReportComponent.builder;
 
 public class IntegrateIssuesVisitorTest {
@@ -103,9 +106,11 @@ public class IntegrateIssuesVisitorTest {
 
   ArgumentCaptor<DefaultIssue> defaultIssueCaptor = ArgumentCaptor.forClass(DefaultIssue.class);
 
+  IssueFilter issueFilter = mock(IssueFilter.class);
+
   BaseIssuesLoader baseIssuesLoader = new BaseIssuesLoader(treeRootHolder, dbTester.getDbClient(), ruleRepositoryRule, activeRulesHolderRule);
   TrackerExecution tracker = new TrackerExecution(new TrackerBaseInputFactory(baseIssuesLoader, dbTester.getDbClient()), new TrackerRawInputFactory(treeRootHolder, reportReader,
-    fileSourceRepository, new CommonRuleEngineImpl()), new Tracker<DefaultIssue, DefaultIssue>());
+    fileSourceRepository, new CommonRuleEngineImpl(), issueFilter), new Tracker<DefaultIssue, DefaultIssue>());
   IssueCache issueCache;
 
   IssueLifecycle issueLifecycle = mock(IssueLifecycle.class);
@@ -119,6 +124,7 @@ public class IntegrateIssuesVisitorTest {
   public void setUp() throws Exception {
     treeRootHolder.setRoot(PROJECT);
     issueCache = new IssueCache(temp.newFile(), System2.INSTANCE);
+    when(issueFilter.accept(any(DefaultIssue.class), eq(FILE))).thenReturn(true);
     underTest = new IntegrateIssuesVisitor(tracker, issueCache, issueLifecycle, issueVisitors, componentsWithUnprocessedIssues, componentIssuesRepository);
   }
 
index 240c547dc0566420d765bbae5a8ca62905c93ec7..0d3491925d7d115bebf9591c15a990bb41eed0ac 100644 (file)
@@ -36,11 +36,14 @@ import org.sonar.server.computation.batch.TreeRootHolderRule;
 import org.sonar.server.computation.component.Component;
 import org.sonar.server.computation.component.ReportComponent;
 import org.sonar.server.computation.issue.commonrule.CommonRuleEngine;
+import org.sonar.server.computation.issue.filter.IssueFilter;
 import org.sonar.server.computation.source.SourceLinesRepositoryRule;
 import org.sonar.server.rule.CommonRuleKeys;
 
 import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -62,7 +65,9 @@ public class TrackerRawInputFactoryTest {
 
   CommonRuleEngine commonRuleEngine = mock(CommonRuleEngine.class);
 
-  TrackerRawInputFactory underTest = new TrackerRawInputFactory(treeRootHolder, reportReader, fileSourceRepository, commonRuleEngine);
+  IssueFilter issueFilter = mock(IssueFilter.class);
+
+  TrackerRawInputFactory underTest = new TrackerRawInputFactory(treeRootHolder, reportReader, fileSourceRepository, commonRuleEngine, issueFilter);
 
   @Test
   public void load_source_hash_sequences() throws Exception {
@@ -86,7 +91,8 @@ public class TrackerRawInputFactoryTest {
   }
 
   @Test
-  public void load_issues() throws Exception {
+  public void load_issues_from_report() throws Exception {
+    when(issueFilter.accept(any(DefaultIssue.class), eq(FILE))).thenReturn(true);
     fileSourceRepository.addLines(FILE_REF, "line 1;", "line 2;");
     ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
       .setTextRange(TextRange.newBuilder().setStartLine(2).build())
@@ -117,6 +123,25 @@ public class TrackerRawInputFactoryTest {
     assertInitializedIssue(issue);
   }
 
+  @Test
+  public void ignore_issue_from_report() throws Exception {
+    when(issueFilter.accept(any(DefaultIssue.class), eq(FILE))).thenReturn(false);
+    fileSourceRepository.addLines(FILE_REF, "line 1;", "line 2;");
+    ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
+      .setTextRange(TextRange.newBuilder().setStartLine(2).build())
+      .setMsg("the message")
+      .setRuleRepository("java")
+      .setRuleKey("S001")
+      .setSeverity(Constants.Severity.BLOCKER)
+      .setGap(3.14)
+      .build();
+    reportReader.putIssues(FILE.getReportAttributes().getRef(), asList(reportIssue));
+    Input<DefaultIssue> input = underTest.create(FILE);
+
+    Collection<DefaultIssue> issues = input.getIssues();
+    assertThat(issues).isEmpty();
+  }
+
   @Test
   public void ignore_report_issues_on_common_rules() throws Exception {
     fileSourceRepository.addLines(FILE_REF, "line 1;", "line 2;");
@@ -135,6 +160,7 @@ public class TrackerRawInputFactoryTest {
 
   @Test
   public void load_issues_of_compute_engine_common_rules() throws Exception {
+    when(issueFilter.accept(any(DefaultIssue.class), eq(FILE))).thenReturn(true);
     fileSourceRepository.addLines(FILE_REF, "line 1;", "line 2;");
     DefaultIssue ceIssue = new DefaultIssue()
       .setRuleKey(RuleKey.of(CommonRuleKeys.commonRepositoryForLang("java"), "InsufficientCoverage"))
@@ -148,6 +174,21 @@ public class TrackerRawInputFactoryTest {
     assertInitializedIssue(input.getIssues().iterator().next());
   }
 
+  @Test
+  public void ignore_issue_from_common_rule() throws Exception {
+    when(issueFilter.accept(any(DefaultIssue.class), eq(FILE))).thenReturn(false);
+    fileSourceRepository.addLines(FILE_REF, "line 1;", "line 2;");
+    DefaultIssue ceIssue = new DefaultIssue()
+      .setRuleKey(RuleKey.of(CommonRuleKeys.commonRepositoryForLang("java"), "InsufficientCoverage"))
+      .setMessage("not enough coverage")
+      .setGap(10.0);
+    when(commonRuleEngine.process(FILE)).thenReturn(asList(ceIssue));
+
+    Input<DefaultIssue> input = underTest.create(FILE);
+
+    assertThat(input.getIssues()).isEmpty();
+  }
+
   private void assertInitializedIssue(DefaultIssue issue) {
     assertThat(issue.componentKey()).isEqualTo(FILE.getKey());
     assertThat(issue.componentUuid()).isEqualTo(FILE.getUuid());
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/issue/filter/IssueFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/issue/filter/IssueFilterTest.java
new file mode 100644 (file)
index 0000000..e4fae82
--- /dev/null
@@ -0,0 +1,215 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.issue.filter;
+
+import com.google.common.base.Joiner;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.Settings;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.server.computation.batch.TreeRootHolderRule;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.SettingsRepository;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.computation.component.Component.Type.FILE;
+import static org.sonar.server.computation.component.ReportComponent.builder;
+
+public class IssueFilterTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  static final RuleKey XOO_X1 = RuleKey.of("xoo", "x1");
+  static final RuleKey XOO_X2 = RuleKey.of("xoo", "x2");
+  static final RuleKey XOO_X3 = RuleKey.of("xoo", "x3");
+
+  static final String PATH1 = "src/main/xoo/File1.xoo";
+  static final String PATH2 = "src/main/xoo/File2.xoo";
+  static final String PATH3 = "src/main/xoo/File3.xoo";
+
+  static final Component PROJECT = builder(Component.Type.PROJECT, 10).build();
+
+  static final Component COMPONENT_1 = builder(FILE, 1).setKey("File1").setPath(PATH1).build();
+  static final Component COMPONENT_2 = builder(FILE, 2).setKey("File2").setPath(PATH2).build();
+  static final Component COMPONENT_3 = builder(FILE, 3).setKey("File3").setPath(PATH3).build();
+
+  static final DefaultIssue ISSUE_1 = new DefaultIssue().setRuleKey(XOO_X1);
+  static final DefaultIssue ISSUE_2 = new DefaultIssue().setRuleKey(XOO_X2);
+  static final DefaultIssue ISSUE_3 = new DefaultIssue().setRuleKey(XOO_X3);
+
+  @Rule
+  public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule().setRoot(PROJECT);
+
+  SettingsRepository settingsRepository = mock(SettingsRepository.class);
+
+  @Test
+  public void accept_everything_when_no_filter_properties() throws Exception {
+    IssueFilter underTest = newIssueFilter(new Settings());
+
+    assertThat(underTest.accept(ISSUE_1, COMPONENT_1)).isTrue();
+    assertThat(underTest.accept(ISSUE_2, COMPONENT_2)).isTrue();
+    assertThat(underTest.accept(ISSUE_3, COMPONENT_3)).isTrue();
+  }
+
+  @Test
+  public void ignore_all() throws Exception {
+    IssueFilter underTest = newIssueFilter(newSettings(asList("*", "**"), Collections.<String>emptyList()));
+
+    assertThat(underTest.accept(ISSUE_1, COMPONENT_1)).isFalse();
+    assertThat(underTest.accept(ISSUE_2, COMPONENT_1)).isFalse();
+    assertThat(underTest.accept(ISSUE_3, COMPONENT_1)).isFalse();
+  }
+
+  @Test
+  public void ignore_some_rule_and_component() throws Exception {
+    IssueFilter underTest = newIssueFilter(newSettings(asList("xoo:x1", "**/xoo/File1*"), Collections.<String>emptyList()));
+
+    assertThat(underTest.accept(ISSUE_1, COMPONENT_1)).isFalse();
+    assertThat(underTest.accept(ISSUE_1, COMPONENT_2)).isTrue();
+    assertThat(underTest.accept(ISSUE_2, COMPONENT_1)).isTrue();
+    assertThat(underTest.accept(ISSUE_2, COMPONENT_2)).isTrue();
+  }
+
+  @Test
+  public void ignore_many_rules() throws Exception {
+    IssueFilter underTest = newIssueFilter(newSettings(
+      asList("xoo:x1", "**/xoo/File1*", "xoo:x2", "**/xoo/File1*"),
+      Collections.<String>emptyList())
+    );
+
+    assertThat(underTest.accept(ISSUE_1, COMPONENT_1)).isFalse();
+    assertThat(underTest.accept(ISSUE_1, COMPONENT_2)).isTrue();
+    assertThat(underTest.accept(ISSUE_2, COMPONENT_1)).isFalse();
+    assertThat(underTest.accept(ISSUE_2, COMPONENT_2)).isTrue();
+  }
+
+  @Test
+  public void include_all() throws Exception {
+    IssueFilter underTest = newIssueFilter(newSettings(Collections.<String>emptyList(), asList("*", "**")));
+
+    assertThat(underTest.accept(ISSUE_1, COMPONENT_1)).isTrue();
+    assertThat(underTest.accept(ISSUE_2, COMPONENT_1)).isTrue();
+    assertThat(underTest.accept(ISSUE_3, COMPONENT_1)).isTrue();
+  }
+
+  @Test
+  public void include_some_rule_and_component() throws Exception {
+    IssueFilter underTest = newIssueFilter(newSettings(Collections.<String>emptyList(), asList("xoo:x1", "**/xoo/File1*")));
+
+    assertThat(underTest.accept(ISSUE_1, COMPONENT_1)).isTrue();
+    assertThat(underTest.accept(ISSUE_1, COMPONENT_2)).isFalse();
+    // Issues on other rule are accepted
+    assertThat(underTest.accept(ISSUE_2, COMPONENT_1)).isTrue();
+    assertThat(underTest.accept(ISSUE_2, COMPONENT_2)).isTrue();
+  }
+
+  @Test
+  public void ignore_and_include_same_rule_and_component() throws Exception {
+    IssueFilter underTest = newIssueFilter(newSettings(
+      asList("xoo:x1", "**/xoo/File1*"),
+      asList("xoo:x1", "**/xoo/File1*")));
+
+    assertThat(underTest.accept(ISSUE_1, COMPONENT_1)).isFalse();
+    assertThat(underTest.accept(ISSUE_1, COMPONENT_2)).isFalse();
+    // Issues on other rule are accepted
+    assertThat(underTest.accept(ISSUE_2, COMPONENT_1)).isTrue();
+    assertThat(underTest.accept(ISSUE_2, COMPONENT_2)).isTrue();
+  }
+
+  @Test
+  public void include_many_rules() throws Exception {
+    IssueFilter underTest = newIssueFilter(newSettings(
+      Collections.<String>emptyList(),
+      asList("xoo:x1", "**/xoo/File1*", "xoo:x2", "**/xoo/File1*")
+    ));
+
+    assertThat(underTest.accept(ISSUE_1, COMPONENT_1)).isTrue();
+    assertThat(underTest.accept(ISSUE_1, COMPONENT_2)).isFalse();
+    assertThat(underTest.accept(ISSUE_2, COMPONENT_1)).isTrue();
+    assertThat(underTest.accept(ISSUE_2, COMPONENT_2)).isFalse();
+  }
+
+  @Test
+  public void accept_project_issues() throws Exception {
+    IssueFilter underTest = newIssueFilter(newSettings(
+      asList("xoo:x1", "**/xoo/File1*"),
+      asList("xoo:x1", "**/xoo/File1*")));
+
+    assertThat(underTest.accept(ISSUE_1, PROJECT)).isTrue();
+    assertThat(underTest.accept(ISSUE_2, PROJECT)).isTrue();
+  }
+
+  @Test
+  public void fail_when_only_rule_key_parameter() throws Exception {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("File path pattern cannot be empty. Please check 'sonar.issue.ignore.multicriteria' settings");
+
+    newIssueFilter(newSettings(asList("xoo:x1", ""), Collections.<String>emptyList()));
+  }
+
+  @Test
+  public void fail_when_only_path_parameter() throws Exception {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Rule key pattern cannot be empty. Please check 'sonar.issue.enforce.multicriteria' settings");
+
+    newIssueFilter(newSettings(Collections.<String>emptyList(), asList("", "**")));
+  }
+
+  private IssueFilter newIssueFilter(Settings settings) {
+    when(settingsRepository.getSettings(PROJECT)).thenReturn(settings);
+    return new IssueFilter(treeRootHolder, settingsRepository);
+  }
+
+  private static Settings newSettings(List<String> exclusionsProperties, List<String> inclusionsProperties) {
+    Settings settings = new Settings();
+    if (!exclusionsProperties.isEmpty()) {
+      addProperties(exclusionsProperties, "ignore", settings);
+    }
+    if (!inclusionsProperties.isEmpty()) {
+      addProperties(inclusionsProperties, "enforce", settings);
+    }
+    return settings;
+  }
+
+  private static void addProperties(List<String> properties, String property, Settings settings) {
+    if (!properties.isEmpty()) {
+      List<Integer> indexes = new ArrayList<>();
+      int index = 1;
+      for (int i = 0; i < properties.size(); i += 2) {
+        settings.setProperty("sonar.issue." + property + ".multicriteria." + index + ".ruleKey", properties.get(i));
+        settings.setProperty("sonar.issue." + property + ".multicriteria." + index + ".resourceKey", properties.get(i + 1));
+        indexes.add(index);
+        index++;
+      }
+      settings.setProperty("sonar.issue." + property + ".multicriteria", Joiner.on(",").join(indexes));
+    }
+  }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/issue/filter/IssuePatternTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/issue/filter/IssuePatternTest.java
new file mode 100644 (file)
index 0000000..e0ace6a
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.issue.filter;
+
+import org.junit.Test;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.Rule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssuePatternTest {
+
+  @Test
+  public void match_file() {
+    String javaFile = "org/foo/Bar.xoo";
+    assertThat(new IssuePattern("org/foo/Bar*", "*").matchComponent(javaFile)).isTrue();
+    assertThat(new IssuePattern("org/foo/*", "*").matchComponent(javaFile)).isTrue();
+    assertThat(new IssuePattern("**/*ar*", "*").matchComponent(javaFile)).isTrue();
+    assertThat(new IssuePattern("org/**/?ar.xoo", "*").matchComponent(javaFile)).isTrue();
+    assertThat(new IssuePattern("**", "*").matchComponent(javaFile)).isTrue();
+
+    assertThat(new IssuePattern("org/other/Hello", "*").matchComponent(javaFile)).isFalse();
+    assertThat(new IssuePattern("org/foo/Hello", "*").matchComponent(javaFile)).isFalse();
+    assertThat(new IssuePattern("org/**/??ar.xoo", "*").matchComponent(javaFile)).isFalse();
+    assertThat(new IssuePattern("org/**/??ar.xoo", "*").matchComponent(null)).isFalse();
+    assertThat(new IssuePattern("org/**/??ar.xoo", "*").matchComponent("plop")).isFalse();
+  }
+
+  @Test
+  public void match_rule() {
+    RuleKey rule = Rule.create("checkstyle", "IllegalRegexp", "").ruleKey();
+    assertThat(new IssuePattern("*", "*").matchRule(rule)).isTrue();
+    assertThat(new IssuePattern("*", "checkstyle:*").matchRule(rule)).isTrue();
+    assertThat(new IssuePattern("*", "checkstyle:IllegalRegexp").matchRule(rule)).isTrue();
+    assertThat(new IssuePattern("*", "checkstyle:Illegal*").matchRule(rule)).isTrue();
+    assertThat(new IssuePattern("*", "*:*Illegal*").matchRule(rule)).isTrue();
+
+    assertThat(new IssuePattern("*", "pmd:IllegalRegexp").matchRule(rule)).isFalse();
+    assertThat(new IssuePattern("*", "pmd:*").matchRule(rule)).isFalse();
+    assertThat(new IssuePattern("*", "*:Foo*IllegalRegexp").matchRule(rule)).isFalse();
+  }
+
+  @Test
+  public void test_to_string() {
+    IssuePattern pattern = new IssuePattern("*", "checkstyle:*");
+
+    assertThat(pattern.toString()).isEqualTo(
+      "IssuePattern{componentPattern=*, rulePattern=checkstyle:*}");
+  }
+}