From 10955ec396a053d596a40ac3fdf7ca73dd911327 Mon Sep 17 00:00:00 2001 From: Julien HENRY Date: Thu, 24 Mar 2016 15:53:36 +0100 Subject: [PATCH] SONAR-7458 Expose SQ Version in SensorContext --- .../main/java/org/sonar/xoo/XooPlugin.java | 20 ++-- .../sonar/xoo/rule/OneIssuePerLineSensor.java | 16 +-- .../java/org/sonar/xoo/XooPluginTest.java | 15 ++- .../xoo/rule/OneIssuePerLineSensorTest.java | 101 +++++++++--------- .../container/ComputeEngineContainerImpl.java | 4 +- .../platformlevel/PlatformLevel1.java | 8 +- .../java/org/sonar/api/SonarQubeVersion.java | 26 ++++- .../sonar/api/batch/sensor/SensorContext.java | 6 ++ .../sensor/internal/SensorContextTester.java | 40 ++++++- .../api/internal/SonarQubeVersionFactory.java | 29 ++--- .../java/org/sonar/api/utils/Version.java | 4 + .../org/sonar/api/SonarQubeVersionTest.java | 6 ++ .../internal/SonarQubeVersionFactoryTest.java | 24 ++--- .../batch/bootstrap/GlobalContainer.java | 4 +- .../deprecated/DeprecatedSensorContext.java | 6 +- .../batch/sensor/DefaultSensorContext.java | 12 ++- .../sensor/DefaultSensorContextTest.java | 7 +- 17 files changed, 210 insertions(+), 118 deletions(-) rename sonar-core/src/main/java/org/sonar/core/platform/SonarQubeVersionProvider.java => sonar-plugin-api/src/main/java/org/sonar/api/internal/SonarQubeVersionFactory.java (64%) rename sonar-core/src/test/java/org/sonar/core/platform/SonarQubeVersionProviderTest.java => sonar-plugin-api/src/test/java/org/sonar/api/internal/SonarQubeVersionFactoryTest.java (74%) diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java index 679d3c10897..74699697e87 100644 --- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java @@ -19,9 +19,7 @@ */ package org.sonar.xoo; -import java.util.Arrays; -import java.util.List; -import org.sonar.api.SonarPlugin; +import org.sonar.api.Plugin; import org.sonar.xoo.coverage.ItCoverageSensor; import org.sonar.xoo.coverage.OverallCoverageSensor; import org.sonar.xoo.coverage.UtCoverageSensor; @@ -60,17 +58,16 @@ import org.sonar.xoo.scm.XooScmProvider; import org.sonar.xoo.test.CoveragePerTestSensor; import org.sonar.xoo.test.TestExecutionSensor; +import static org.sonar.api.SonarQubeVersion.V5_5; + /** * Plugin entry-point, as declared in pom.xml. */ -public class XooPlugin extends SonarPlugin { +public class XooPlugin implements Plugin { - /** - * Declares all the extensions implemented in the plugin - */ @Override - public List getExtensions() { - return Arrays.asList( + public void define(Context context) { + context.addExtensions( Xoo.class, Xoo2.class, XooRulesDefinition.class, @@ -98,7 +95,6 @@ public class XooPlugin extends SonarPlugin { ChecksSensor.class, RandomAccessSensor.class, DeprecatedResourceApiSensor.class, - CpdTokenizerSensor.class, OneBlockerIssuePerFileSensor.class, OneIssuePerLineSensor.class, @@ -125,6 +121,10 @@ public class XooPlugin extends SonarPlugin { // Other XooProjectBuilder.class, XooPostJob.class); + + if (context.getSonarQubeVersion().isGreaterThanOrEqual(V5_5)) { + context.addExtension(CpdTokenizerSensor.class); + } } } diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneIssuePerLineSensor.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneIssuePerLineSensor.java index cb1e685803a..2b9474ec391 100644 --- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneIssuePerLineSensor.java +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneIssuePerLineSensor.java @@ -19,8 +19,7 @@ */ package org.sonar.xoo.rule; -import org.sonar.xoo.Xoo2; - +import org.sonar.api.SonarQubeVersion; import org.sonar.api.batch.fs.FilePredicates; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.fs.InputFile; @@ -32,11 +31,12 @@ import org.sonar.api.batch.sensor.SensorDescriptor; import org.sonar.api.batch.sensor.issue.NewIssue; import org.sonar.api.rule.RuleKey; import org.sonar.xoo.Xoo; +import org.sonar.xoo.Xoo2; public class OneIssuePerLineSensor implements Sensor { public static final String RULE_KEY = "OneIssuePerLine"; - private static final String EFFORT_TO_FIX_PROPERTY = "sonar.oneIssuePerLine.effortToFix"; + public static final String EFFORT_TO_FIX_PROPERTY = "sonar.oneIssuePerLine.effortToFix"; public static final String FORCE_SEVERITY_PROPERTY = "sonar.oneIssuePerLine.forceSeverity"; @Override @@ -72,9 +72,13 @@ public class OneIssuePerLineSensor implements Sensor { .on(file) .at(file.selectLine(line)) .message("This issue is generated on each line")) - .effortToFix(context.settings().getDouble(EFFORT_TO_FIX_PROPERTY)) - .overrideSeverity(severity != null ? Severity.valueOf(severity) : null) - .save(); + .overrideSeverity(severity != null ? Severity.valueOf(severity) : null); + if (context.getSonarQubeVersion().isGreaterThanOrEqual(SonarQubeVersion.V5_5)) { + newIssue.gap(context.settings().getDouble(EFFORT_TO_FIX_PROPERTY)); + } else { + newIssue.effortToFix(context.settings().getDouble(EFFORT_TO_FIX_PROPERTY)); + } + newIssue.save(); } } diff --git a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java index f31e1b667d8..69cc0823895 100644 --- a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java +++ b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java @@ -20,13 +20,24 @@ package org.sonar.xoo; import org.junit.Test; +import org.sonar.api.Plugin; +import org.sonar.api.SonarQubeVersion; +import org.sonar.api.utils.Version; +import org.sonar.xoo.lang.CpdTokenizerSensor; import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.SonarQubeVersion.V5_5; public class XooPluginTest { @Test - public void provide_extensions() { - assertThat(new XooPlugin().getExtensions().size()).isGreaterThan(0); + public void provide_extensions_for_5_5() { + Plugin.Context context = new Plugin.Context(new SonarQubeVersion(V5_5)); + new XooPlugin().define(context); + assertThat(context.getExtensions()).hasSize(39).contains(CpdTokenizerSensor.class); + + context = new Plugin.Context(new SonarQubeVersion(Version.parse("5.4"))); + new XooPlugin().define(context); + assertThat(context.getExtensions()).hasSize(38).doesNotContain(CpdTokenizerSensor.class); } } diff --git a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/rule/OneIssuePerLineSensorTest.java b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/rule/OneIssuePerLineSensorTest.java index e225fd5e71c..8993bac751e 100644 --- a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/rule/OneIssuePerLineSensorTest.java +++ b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/rule/OneIssuePerLineSensorTest.java @@ -24,26 +24,16 @@ import java.io.StringReader; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import org.mockito.ArgumentCaptor; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.sonar.api.batch.fs.internal.DefaultFileSystem; import org.sonar.api.batch.fs.internal.DefaultInputFile; import org.sonar.api.batch.fs.internal.FileMetadata; import org.sonar.api.batch.rule.Severity; -import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.batch.sensor.internal.DefaultSensorDescriptor; -import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.batch.sensor.internal.SensorContextTester; import org.sonar.api.batch.sensor.issue.Issue; -import org.sonar.api.batch.sensor.issue.internal.DefaultIssue; -import org.sonar.api.config.Settings; +import org.sonar.api.utils.Version; import org.sonar.xoo.Xoo; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; public class OneIssuePerLineSensorTest { @@ -61,54 +51,69 @@ public class OneIssuePerLineSensorTest { @Test public void testRule() throws IOException { - DefaultFileSystem fs = new DefaultFileSystem(temp.newFolder().toPath()); DefaultInputFile inputFile = new DefaultInputFile("foo", "src/Foo.xoo").setLanguage(Xoo.KEY) .initMetadata(new FileMetadata().readMetadata(new StringReader("a\nb\nc\nd\ne\nf\ng\nh\ni\n"))); - fs.add(inputFile); - - SensorContext context = mock(SensorContext.class); - final SensorStorage sensorStorage = mock(SensorStorage.class); - when(context.settings()).thenReturn(new Settings()); - when(context.fileSystem()).thenReturn(fs); - when(context.newIssue()).thenAnswer(new Answer() { - @Override - public Issue answer(InvocationOnMock invocation) throws Throwable { - return new DefaultIssue(sensorStorage); - } - }); + + SensorContextTester context = SensorContextTester.create(temp.newFolder()); + context.fileSystem().add(inputFile); sensor.execute(context); - ArgumentCaptor argCaptor = ArgumentCaptor.forClass(DefaultIssue.class); - verify(sensorStorage, times(10)).store(argCaptor.capture()); - assertThat(argCaptor.getAllValues()).hasSize(10); // One issue per line - assertThat(argCaptor.getValue().overriddenSeverity()).isNull(); + assertThat(context.allIssues()).hasSize(10); // One issue per line + for (Issue issue : context.allIssues()) { + assertThat(issue.gap()).isNull(); + } } @Test public void testForceSeverity() throws IOException { - DefaultFileSystem fs = new DefaultFileSystem(temp.newFolder().toPath()); DefaultInputFile inputFile = new DefaultInputFile("foo", "src/Foo.xoo").setLanguage(Xoo.KEY) .initMetadata(new FileMetadata().readMetadata(new StringReader("a\nb\nc\nd\ne\nf\ng\nh\ni\n"))); - fs.add(inputFile); - - SensorContext context = mock(SensorContext.class); - final SensorStorage sensorStorage = mock(SensorStorage.class); - Settings settings = new Settings(); - settings.setProperty(OneIssuePerLineSensor.FORCE_SEVERITY_PROPERTY, "MINOR"); - when(context.settings()).thenReturn(settings); - when(context.fileSystem()).thenReturn(fs); - when(context.newIssue()).thenAnswer(new Answer() { - @Override - public Issue answer(InvocationOnMock invocation) throws Throwable { - return new DefaultIssue(sensorStorage); - } - }); + + SensorContextTester context = SensorContextTester.create(temp.newFolder()); + context.fileSystem().add(inputFile); + context.settings().setProperty(OneIssuePerLineSensor.FORCE_SEVERITY_PROPERTY, "MINOR"); + + sensor.execute(context); + + assertThat(context.allIssues()).hasSize(10); // One issue per line + for (Issue issue : context.allIssues()) { + assertThat(issue.overriddenSeverity()).isEqualTo(Severity.MINOR); + } + } + + @Test + public void testProvideGap() throws IOException { + DefaultInputFile inputFile = new DefaultInputFile("foo", "src/Foo.xoo").setLanguage(Xoo.KEY) + .initMetadata(new FileMetadata().readMetadata(new StringReader("a\nb\nc\nd\ne\nf\ng\nh\ni\n"))); + + SensorContextTester context = SensorContextTester.create(temp.newFolder()); + context.fileSystem().add(inputFile); + context.settings().setProperty(OneIssuePerLineSensor.EFFORT_TO_FIX_PROPERTY, "1.2"); + + sensor.execute(context); + + assertThat(context.allIssues()).hasSize(10); // One issue per line + for (Issue issue : context.allIssues()) { + assertThat(issue.gap()).isEqualTo(1.2d); + } + } + + @Test + public void testProvideGap_before_5_5() throws IOException { + DefaultInputFile inputFile = new DefaultInputFile("foo", "src/Foo.xoo").setLanguage(Xoo.KEY) + .initMetadata(new FileMetadata().readMetadata(new StringReader("a\nb\nc\nd\ne\nf\ng\nh\ni\n"))); + + SensorContextTester context = SensorContextTester.create(temp.newFolder()); + context.fileSystem().add(inputFile); + context.settings().setProperty(OneIssuePerLineSensor.EFFORT_TO_FIX_PROPERTY, "1.2"); + context.setSonarQubeVersion(Version.parse("5.4")); + sensor.execute(context); - ArgumentCaptor argCaptor = ArgumentCaptor.forClass(DefaultIssue.class); - verify(sensorStorage, times(10)).store(argCaptor.capture()); - assertThat(argCaptor.getAllValues()).hasSize(10); // One issue per line - assertThat(argCaptor.getValue().overriddenSeverity()).isEqualTo(Severity.MINOR); + assertThat(context.allIssues()).hasSize(10); // One issue per line + for (Issue issue : context.allIssues()) { + assertThat(issue.gap()).isEqualTo(1.2d); + } } } diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java index ca6cc5bc496..9a02f215935 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java @@ -23,6 +23,7 @@ import com.google.common.annotations.VisibleForTesting; import java.util.List; import javax.annotation.CheckForNull; import org.sonar.api.config.EmailSettings; +import org.sonar.api.internal.SonarQubeVersionFactory; import org.sonar.api.profiles.AnnotationProfileParser; import org.sonar.api.profiles.XMLProfileParser; import org.sonar.api.profiles.XMLProfileSerializer; @@ -49,7 +50,6 @@ import org.sonar.core.platform.ComponentContainer; import org.sonar.core.platform.Module; import org.sonar.core.platform.PluginClassloaderFactory; import org.sonar.core.platform.PluginLoader; -import org.sonar.core.platform.SonarQubeVersionProvider; import org.sonar.core.timemachine.Periods; import org.sonar.core.user.DefaultUserFinder; import org.sonar.core.user.DeprecatedUserFinder; @@ -138,7 +138,7 @@ import org.sonarqube.ws.Rules; public class ComputeEngineContainerImpl implements ComputeEngineContainer { private static final Object[] LEVEL_1_COMPONENTS = new Object[] { ComputeEngineSettings.class, - new SonarQubeVersionProvider(), + SonarQubeVersionFactory.create(System2.INSTANCE), new JmxConnectionFactoryProvider(), ServerImpl.class, UuidFactoryImpl.INSTANCE, diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java index ae0469e3237..a64ef830bf2 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java @@ -21,10 +21,11 @@ package org.sonar.server.platform.platformlevel; import java.util.Properties; import javax.annotation.Nullable; +import org.sonar.api.internal.SonarQubeVersionFactory; import org.sonar.api.utils.System2; import org.sonar.api.utils.internal.TempFolderCleaner; +import org.sonar.ce.property.CePropertyDefinitions; import org.sonar.core.config.CorePropertyDefinitions; -import org.sonar.core.platform.SonarQubeVersionProvider; import org.sonar.core.util.UuidFactoryImpl; import org.sonar.db.DaoModule; import org.sonar.db.DatabaseChecker; @@ -35,15 +36,14 @@ import org.sonar.db.semaphore.SemaphoresImpl; import org.sonar.db.version.DatabaseVersion; import org.sonar.db.version.MigrationStepModule; import org.sonar.server.app.ProcessCommandWrapperImpl; -import org.sonar.ce.property.CePropertyDefinitions; import org.sonar.server.db.EmbeddedDatabaseFactory; import org.sonar.server.issue.index.IssueIndex; import org.sonar.server.platform.DatabaseServerCompatibility; import org.sonar.server.platform.DefaultServerFileSystem; import org.sonar.server.platform.Platform; import org.sonar.server.platform.ServerImpl; -import org.sonar.server.platform.WebServerSettings; import org.sonar.server.platform.TempFolderProvider; +import org.sonar.server.platform.WebServerSettings; import org.sonar.server.qualityprofile.index.ActiveRuleIndex; import org.sonar.server.ruby.PlatformRackBridge; import org.sonar.server.rule.index.RuleIndex; @@ -68,7 +68,7 @@ public class PlatformLevel1 extends PlatformLevel { add(platform, properties); addExtraRootComponents(); add( - new SonarQubeVersionProvider(), + SonarQubeVersionFactory.create(System2.INSTANCE), ProcessCommandWrapperImpl.class, WebServerSettings.class, ServerImpl.class, diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/SonarQubeVersion.java b/sonar-plugin-api/src/main/java/org/sonar/api/SonarQubeVersion.java index 0889546d04e..5387d3ef765 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/SonarQubeVersion.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/SonarQubeVersion.java @@ -21,6 +21,7 @@ package org.sonar.api; import javax.annotation.concurrent.Immutable; import org.sonar.api.batch.BatchSide; +import org.sonar.api.batch.sensor.Sensor; import org.sonar.api.ce.ComputeEngineSide; import org.sonar.api.server.ServerSide; import org.sonar.api.utils.Version; @@ -31,10 +32,24 @@ import static java.util.Objects.requireNonNull; * Version of SonarQube at runtime. This component can be injected as a dependency * of plugin extensions. The main usage for a plugin is to benefit from new APIs * while keeping backward-compatibility with previous versions of SonarQube. - - * Example: a plugin needs to use an API introduced in version 5.6 ({@code AnApi} in the following + *

+ * + * Example 1: a {@link Sensor} wants to use an API introduced in version 5.5 and still requires to support older versions + * at runtime. + *
+ * public class MySensor implements Sensor {
+ *
+ *   public void execute(SensorContext context) {
+ *     if (context.getSonarQubeVersion().isGreaterThanOrEqual(SonarQubeVersion.V5_5)) {
+ *       context.newMethodIntroducedIn5_5();
+ *     }
+ *   }
+ * }
+ * 
+ * + * Example 2: a plugin needs to use an API introduced in version 5.6 ({@code AnApi} in the following * snippet) and still requires to support version 5.5 at runtime. - *

+ *
*
  * // Component provided by sonar-plugin-api
  * // @since 5.5
@@ -45,7 +60,7 @@ import static java.util.Objects.requireNonNull;
  *   // @since 5.6
  *   public void bar();
  * }
- *
+ * 
  * // Component provided by plugin
  * public class MyExtension {
  *   private final SonarQubeVersion sonarQubeVersion;
@@ -131,6 +146,7 @@ public class SonarQubeVersion {
   }
 
   public boolean isGreaterThanOrEqual(Version than) {
-    return this.version.compareTo(than) >= 0;
+    return this.version.isGreaterThanOrEqual(than);
   }
+
 }
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java
index 3d49859470e..1c159c38b78 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java
@@ -33,6 +33,7 @@ import org.sonar.api.batch.sensor.issue.NewIssue;
 import org.sonar.api.batch.sensor.measure.Measure;
 import org.sonar.api.batch.sensor.measure.NewMeasure;
 import org.sonar.api.config.Settings;
+import org.sonar.api.utils.Version;
 
 /**
  * See {@link Sensor#execute(SensorContext)}
@@ -62,6 +63,11 @@ public interface SensorContext {
    */
   InputModule module();
 
+  /**
+   * @since 5.5
+   */
+  Version getSonarQubeVersion();
+
   // ----------- MEASURES --------------
 
   /**
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java
index 24a06a241bd..b0c19108f16 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java
@@ -24,6 +24,7 @@ import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.Table;
 import java.io.File;
 import java.io.Serializable;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -32,6 +33,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import javax.annotation.CheckForNull;
+import org.sonar.api.SonarQubeVersion;
 import org.sonar.api.batch.AnalysisMode;
 import org.sonar.api.batch.fs.InputModule;
 import org.sonar.api.batch.fs.internal.DefaultFileSystem;
@@ -57,7 +59,10 @@ import org.sonar.api.batch.sensor.measure.Measure;
 import org.sonar.api.batch.sensor.measure.NewMeasure;
 import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
 import org.sonar.api.config.Settings;
+import org.sonar.api.internal.SonarQubeVersionFactory;
 import org.sonar.api.measures.Metric;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.Version;
 import org.sonar.duplications.internal.pmd.TokensLine;
 
 /**
@@ -76,16 +81,22 @@ public class SensorContextTester implements SensorContext {
   private ActiveRules activeRules;
   private InMemorySensorStorage sensorStorage;
   private InputModule module;
+  private SonarQubeVersion sqVersion;
 
-  private SensorContextTester(File moduleBaseDir) {
+  private SensorContextTester(Path moduleBaseDir) {
     this.settings = new Settings();
     this.fs = new DefaultFileSystem(moduleBaseDir);
     this.activeRules = new ActiveRulesBuilder().build();
     this.sensorStorage = new InMemorySensorStorage();
     this.module = new DefaultInputModule("projectKey");
+    this.sqVersion = SonarQubeVersionFactory.create(System2.INSTANCE);
   }
 
   public static SensorContextTester create(File moduleBaseDir) {
+    return new SensorContextTester(moduleBaseDir.toPath());
+  }
+
+  public static SensorContextTester create(Path moduleBaseDir) {
     return new SensorContextTester(moduleBaseDir);
   }
 
@@ -94,8 +105,9 @@ public class SensorContextTester implements SensorContext {
     return settings;
   }
 
-  public void setSettings(Settings settings) {
+  public SensorContextTester setSettings(Settings settings) {
     this.settings = settings;
+    return this;
   }
 
   @Override
@@ -103,8 +115,9 @@ public class SensorContextTester implements SensorContext {
     return fs;
   }
 
-  public void setFileSystem(DefaultFileSystem fs) {
+  public SensorContextTester setFileSystem(DefaultFileSystem fs) {
     this.fs = fs;
+    return this;
   }
 
   @Override
@@ -112,8 +125,27 @@ public class SensorContextTester implements SensorContext {
     return activeRules;
   }
 
-  public void setActiveRules(ActiveRules activeRules) {
+  public SensorContextTester setActiveRules(ActiveRules activeRules) {
     this.activeRules = activeRules;
+    return this;
+  }
+
+  /**
+   * Default value is the version of this API. You can override it
+   * using {@link #setSonarQubeVersion(Version)} to test your Sensor behavior.
+   * @since 5.5
+   */
+  @Override
+  public Version getSonarQubeVersion() {
+    return sqVersion.get();
+  }
+
+  /**
+   * @since 5.5
+   */
+  public SensorContextTester setSonarQubeVersion(Version version) {
+    this.sqVersion = new SonarQubeVersion(version);
+    return this;
   }
 
   @Override
diff --git a/sonar-core/src/main/java/org/sonar/core/platform/SonarQubeVersionProvider.java b/sonar-plugin-api/src/main/java/org/sonar/api/internal/SonarQubeVersionFactory.java
similarity index 64%
rename from sonar-core/src/main/java/org/sonar/core/platform/SonarQubeVersionProvider.java
rename to sonar-plugin-api/src/main/java/org/sonar/api/internal/SonarQubeVersionFactory.java
index a0f54de7490..db85c3feaf1 100644
--- a/sonar-core/src/main/java/org/sonar/core/platform/SonarQubeVersionProvider.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/internal/SonarQubeVersionFactory.java
@@ -17,33 +17,34 @@
  * 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.core.platform;
+package org.sonar.api.internal;
 
 import com.google.common.io.Resources;
 import java.io.IOException;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
-import org.picocontainer.injectors.ProviderAdapter;
 import org.sonar.api.SonarQubeVersion;
 import org.sonar.api.utils.System2;
 import org.sonar.api.utils.Version;
 
-public class SonarQubeVersionProvider extends ProviderAdapter {
+/**
+ * For internal use only.
+ */
+public class SonarQubeVersionFactory {
 
   private static final String FILE_PATH = "/sq-version.txt";
 
-  private SonarQubeVersion version = null;
+  private SonarQubeVersionFactory() {
+    // prevents instantiation
+  }
 
-  public SonarQubeVersion provide(System2 system) {
-    if (version == null) {
-      try {
-        URL url = system.getResource(FILE_PATH);
-        String versionInFile = Resources.toString(url, StandardCharsets.UTF_8);
-        version = new SonarQubeVersion(Version.parse(versionInFile));
-      } catch (IOException e) {
-        throw new IllegalStateException("Can not load " + FILE_PATH + " from classpath", e);
-      }
+  public static SonarQubeVersion create(System2 system) {
+    try {
+      URL url = system.getResource(FILE_PATH);
+      String versionInFile = Resources.toString(url, StandardCharsets.UTF_8);
+      return new SonarQubeVersion(Version.parse(versionInFile));
+    } catch (IOException e) {
+      throw new IllegalStateException("Can not load " + FILE_PATH + " from classpath", e);
     }
-    return version;
   }
 }
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/Version.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/Version.java
index dcd96d9cbb4..50813e97c0e 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/utils/Version.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/utils/Version.java
@@ -144,6 +144,10 @@ public class Version implements Comparable {
     return parseInt(sequence);
   }
 
+  public boolean isGreaterThanOrEqual(Version than) {
+    return this.compareTo(than) >= 0;
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/SonarQubeVersionTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/SonarQubeVersionTest.java
index 0cc786c3ba4..50bc4b1c2ec 100644
--- a/sonar-plugin-api/src/test/java/org/sonar/api/SonarQubeVersionTest.java
+++ b/sonar-plugin-api/src/test/java/org/sonar/api/SonarQubeVersionTest.java
@@ -19,13 +19,18 @@
  */
 package org.sonar.api;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 import org.sonar.api.utils.Version;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
 public class SonarQubeVersionTest {
 
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
   @Test
   public void isGte() {
     Version version = Version.parse("1.2.3");
@@ -35,4 +40,5 @@ public class SonarQubeVersionTest {
     assertThat(qubeVersion.isGreaterThanOrEqual(Version.parse("1.1"))).isTrue();
     assertThat(qubeVersion.isGreaterThanOrEqual(Version.parse("1.3"))).isFalse();
   }
+
 }
diff --git a/sonar-core/src/test/java/org/sonar/core/platform/SonarQubeVersionProviderTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/internal/SonarQubeVersionFactoryTest.java
similarity index 74%
rename from sonar-core/src/test/java/org/sonar/core/platform/SonarQubeVersionProviderTest.java
rename to sonar-plugin-api/src/test/java/org/sonar/api/internal/SonarQubeVersionFactoryTest.java
index 79d9baddc27..d37f7693d10 100644
--- a/sonar-core/src/test/java/org/sonar/core/platform/SonarQubeVersionProviderTest.java
+++ b/sonar-plugin-api/src/test/java/org/sonar/api/internal/SonarQubeVersionFactoryTest.java
@@ -17,7 +17,7 @@
  * 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.core.platform;
+package org.sonar.api.internal;
 
 import java.io.File;
 import org.junit.Rule;
@@ -28,37 +28,29 @@ import org.sonar.api.utils.System2;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Matchers.anyString;
-import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
-public class SonarQubeVersionProviderTest {
+public class SonarQubeVersionFactoryTest {
 
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
 
-  SonarQubeVersionProvider underTest = new SonarQubeVersionProvider();
-
   @Test
   public void create() {
-    SonarQubeVersion version = underTest.provide(System2.INSTANCE);
+    SonarQubeVersion version = SonarQubeVersionFactory.create(System2.INSTANCE);
     assertThat(version).isNotNull();
     assertThat(version.get().major()).isGreaterThanOrEqualTo(5);
   }
 
-  @Test
-  public void cache_version() {
-    SonarQubeVersion version1 = underTest.provide(System2.INSTANCE);
-    SonarQubeVersion version2 = underTest.provide(System2.INSTANCE);
-    assertThat(version1).isSameAs(version2);
-  }
-
   @Test
   public void throw_ISE_if_fail_to_load_version() throws Exception {
     expectedException.expect(IllegalStateException.class);
     expectedException.expectMessage("Can not load /sq-version.txt from classpath");
 
-    System2 system = mock(System2.class);
-    when(system.getResource(anyString())).thenReturn(new File("target/unknown").toURL());
-    underTest.provide(system);
+    System2 system = spy(System2.class);
+    when(system.getResource(anyString())).thenReturn(new File("target/unknown").toURI().toURL());
+    SonarQubeVersionFactory.create(system);
   }
+
 }
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalContainer.java
index ea6e967da42..dcc80d2fff5 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalContainer.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalContainer.java
@@ -22,6 +22,7 @@ package org.sonar.batch.bootstrap;
 import java.util.List;
 import java.util.Map;
 import org.sonar.api.Plugin;
+import org.sonar.api.internal.SonarQubeVersionFactory;
 import org.sonar.api.utils.System2;
 import org.sonar.api.utils.UriReader;
 import org.sonar.batch.cache.GlobalPersistentCacheProvider;
@@ -39,7 +40,6 @@ import org.sonar.core.platform.PluginClassloaderFactory;
 import org.sonar.core.platform.PluginInfo;
 import org.sonar.core.platform.PluginLoader;
 import org.sonar.core.platform.PluginRepository;
-import org.sonar.core.platform.SonarQubeVersionProvider;
 import org.sonar.core.util.DefaultHttpDownloader;
 import org.sonar.core.util.UuidFactoryImpl;
 
@@ -90,7 +90,7 @@ public class GlobalContainer extends ComponentContainer {
       BatchPluginPredicate.class,
       ExtensionInstaller.class,
 
-      new SonarQubeVersionProvider(),
+      SonarQubeVersionFactory.create(System2.INSTANCE),
       CachesManager.class,
       GlobalSettings.class,
       new BatchWsClientProvider(),
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/DeprecatedSensorContext.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/DeprecatedSensorContext.java
index 33a56f3e5ee..91a61dfa3d3 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/DeprecatedSensorContext.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/DeprecatedSensorContext.java
@@ -23,6 +23,7 @@ import java.io.Serializable;
 import java.util.Collection;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.sonar.api.SonarQubeVersion;
 import org.sonar.api.batch.AnalysisMode;
 import org.sonar.api.batch.SensorContext;
 import org.sonar.api.batch.SonarIndex;
@@ -55,9 +56,8 @@ public class DeprecatedSensorContext extends DefaultSensorContext implements Sen
   private final CoverageExclusions coverageFilter;
 
   public DeprecatedSensorContext(InputModule module, SonarIndex index, Project project, Settings settings, FileSystem fs, ActiveRules activeRules,
-    AnalysisMode analysisMode, CoverageExclusions coverageFilter,
-    SensorStorage sensorStorage) {
-    super(module, settings, fs, activeRules, analysisMode, sensorStorage);
+    AnalysisMode analysisMode, CoverageExclusions coverageFilter, SensorStorage sensorStorage, SonarQubeVersion sqVersion) {
+    super(module, settings, fs, activeRules, analysisMode, sensorStorage, sqVersion);
     this.index = index;
     this.project = project;
     this.coverageFilter = coverageFilter;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/DefaultSensorContext.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/DefaultSensorContext.java
index e0a61eb9117..48dde9fd12d 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/DefaultSensorContext.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/DefaultSensorContext.java
@@ -20,6 +20,7 @@
 package org.sonar.batch.sensor;
 
 import java.io.Serializable;
+import org.sonar.api.SonarQubeVersion;
 import org.sonar.api.batch.AnalysisMode;
 import org.sonar.api.batch.fs.FileSystem;
 import org.sonar.api.batch.fs.InputModule;
@@ -37,6 +38,7 @@ import org.sonar.api.batch.sensor.issue.internal.DefaultIssue;
 import org.sonar.api.batch.sensor.measure.NewMeasure;
 import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
 import org.sonar.api.config.Settings;
+import org.sonar.api.utils.Version;
 import org.sonar.batch.sensor.noop.NoOpNewCpdTokens;
 import org.sonar.batch.sensor.noop.NoOpNewHighlighting;
 
@@ -51,14 +53,17 @@ public class DefaultSensorContext implements SensorContext {
   private final SensorStorage sensorStorage;
   private final AnalysisMode analysisMode;
   private final InputModule module;
+  private final SonarQubeVersion sqVersion;
 
-  public DefaultSensorContext(InputModule module, Settings settings, FileSystem fs, ActiveRules activeRules, AnalysisMode analysisMode, SensorStorage sensorStorage) {
+  public DefaultSensorContext(InputModule module, Settings settings, FileSystem fs, ActiveRules activeRules, AnalysisMode analysisMode, SensorStorage sensorStorage,
+    SonarQubeVersion sqVersion) {
     this.module = module;
     this.settings = settings;
     this.fs = fs;
     this.activeRules = activeRules;
     this.analysisMode = analysisMode;
     this.sensorStorage = sensorStorage;
+    this.sqVersion = sqVersion;
   }
 
   @Override
@@ -81,6 +86,11 @@ public class DefaultSensorContext implements SensorContext {
     return module;
   }
 
+  @Override
+  public Version getSonarQubeVersion() {
+    return sqVersion.get();
+  }
+
   @Override
   public  NewMeasure newMeasure() {
     return new DefaultMeasure<>(sensorStorage);
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/DefaultSensorContextTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/DefaultSensorContextTest.java
index ba40858840d..913a5fa67bc 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/DefaultSensorContextTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/DefaultSensorContextTest.java
@@ -24,6 +24,7 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
+import org.sonar.api.SonarQubeVersion;
 import org.sonar.api.batch.AnalysisMode;
 import org.sonar.api.batch.fs.InputModule;
 import org.sonar.api.batch.fs.internal.DefaultFileSystem;
@@ -33,6 +34,7 @@ import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
 import org.sonar.api.batch.sensor.internal.SensorStorage;
 import org.sonar.api.config.Settings;
 import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.utils.Version;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
@@ -52,6 +54,7 @@ public class DefaultSensorContextTest {
   private Settings settings;
   private SensorStorage sensorStorage;
   private AnalysisMode analysisMode;
+  private SonarQubeVersion sqVersion;
 
   @Before
   public void prepare() throws Exception {
@@ -63,7 +66,8 @@ public class DefaultSensorContextTest {
     settings = new Settings();
     sensorStorage = mock(SensorStorage.class);
     analysisMode = mock(AnalysisMode.class);
-    adaptor = new DefaultSensorContext(mock(InputModule.class), settings, fs, activeRules, analysisMode, sensorStorage);
+    sqVersion = new SonarQubeVersion(Version.parse("5.5"));
+    adaptor = new DefaultSensorContext(mock(InputModule.class), settings, fs, activeRules, analysisMode, sensorStorage, sqVersion);
   }
 
   @Test
@@ -71,6 +75,7 @@ public class DefaultSensorContextTest {
     assertThat(adaptor.activeRules()).isEqualTo(activeRules);
     assertThat(adaptor.fileSystem()).isEqualTo(fs);
     assertThat(adaptor.settings()).isEqualTo(settings);
+    assertThat(adaptor.getSonarQubeVersion()).isEqualTo(Version.parse("5.5"));
 
     assertThat(adaptor.newIssue()).isNotNull();
     assertThat(adaptor.newMeasure()).isNotNull();
-- 
2.39.5