From ba7439977f53e5c05f2c6cd52b907f6582a10bde Mon Sep 17 00:00:00 2001 From: David Gageot Date: Mon, 18 Jun 2012 11:47:54 +0200 Subject: [PATCH] SONAR-3516 Check minimal sonar version required by installed plugins Plugin manifest declares the minimal required version of sonar. This version is verified at server startup. It prevents plugins from failing for API incompatibility reasons. Startup fails with a meaningful message. --- .../core/plugins/DefaultPluginMetadata.java | 45 +++++++++ .../sonar/core/plugins/PluginInstaller.java | 1 + .../plugins/DefaultPluginMetadataTest.java | 92 ++++++++++++------ .../core/plugins/PluginInstallerTest.java | 77 ++++++++------- .../checkstyle-extension.xml | 0 ...sonar-switch-off-violations-plugin-1.1.jar | Bin 0 -> 12689 bytes .../sonar/server/plugins/PluginDeployer.java | 49 ++++++---- .../server/plugins/PluginDeployerTest.java | 62 +++++++----- ...sonar-switch-off-violations-plugin-1.1.jar | Bin 0 -> 12689 bytes 9 files changed, 218 insertions(+), 108 deletions(-) rename sonar-core/src/test/resources/org/sonar/core/plugins/{PluginInstallerTest/shouldCopyRuleExtensionsOnServerSide => }/checkstyle-extension.xml (100%) create mode 100644 sonar-core/src/test/resources/org/sonar/core/plugins/sonar-switch-off-violations-plugin-1.1.jar create mode 100644 sonar-server/src/test/resources/org/sonar/server/plugins/PluginDeployerTest/should_fail_on_plugin_depending_on_more_recent_sonar/extensions/plugins/sonar-switch-off-violations-plugin-1.1.jar diff --git a/sonar-core/src/main/java/org/sonar/core/plugins/DefaultPluginMetadata.java b/sonar-core/src/main/java/org/sonar/core/plugins/DefaultPluginMetadata.java index f9f65033cd8..22ff29fadb9 100644 --- a/sonar-core/src/main/java/org/sonar/core/plugins/DefaultPluginMetadata.java +++ b/sonar-core/src/main/java/org/sonar/core/plugins/DefaultPluginMetadata.java @@ -19,6 +19,10 @@ */ package org.sonar.core.plugins; +import com.google.common.base.CharMatcher; +import com.google.common.base.Splitter; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.builder.ToStringBuilder; @@ -35,6 +39,7 @@ public class DefaultPluginMetadata implements PluginMetadata, Comparabletrue if the plugin is compatible + */ + public boolean isCompatibleWith(String sonarVersion) { + if (null == this.sonarVersion) { + return true; // Plugins without sonar version are so old, they are compatible with a version containing this code + } + if (null == sonarVersion) { + return true; + } + + return ComparisonChain.start() + .compare(part(sonarVersion, 0), part(this.sonarVersion, 0)) + .compare(part(sonarVersion, 1), part(this.sonarVersion, 1)) + .compare(part(sonarVersion, 2), part(this.sonarVersion, 2)) + .result() >= 0; + } + + private static int part(String version, int index) { + Iterable parts = Splitter.on('.').split(version); + String part = Iterables.get(parts, index, "0"); + String onlyDigits = CharMatcher.DIGIT.retainFrom(part); + + return Integer.parseInt(onlyDigits); + } + public String getHomepage() { return homepage; } diff --git a/sonar-core/src/main/java/org/sonar/core/plugins/PluginInstaller.java b/sonar-core/src/main/java/org/sonar/core/plugins/PluginInstaller.java index 5566a0e0ae0..d0289f4a82f 100644 --- a/sonar-core/src/main/java/org/sonar/core/plugins/PluginInstaller.java +++ b/sonar-core/src/main/java/org/sonar/core/plugins/PluginInstaller.java @@ -116,6 +116,7 @@ public class PluginInstaller { metadata.setOrganizationUrl(manifest.getOrganizationUrl()); metadata.setMainClass(manifest.getMainClass()); metadata.setVersion(manifest.getVersion()); + metadata.setSonarVersion(manifest.getSonarVersion()); metadata.setHomepage(manifest.getHomepage()); metadata.setPathsToInternalDeps(manifest.getDependencies()); metadata.setUseChildFirstClassLoader(manifest.isUseChildFirstClassLoader()); diff --git a/sonar-core/src/test/java/org/sonar/core/plugins/DefaultPluginMetadataTest.java b/sonar-core/src/test/java/org/sonar/core/plugins/DefaultPluginMetadataTest.java index 27cf28eea34..02053827323 100644 --- a/sonar-core/src/test/java/org/sonar/core/plugins/DefaultPluginMetadataTest.java +++ b/sonar-core/src/test/java/org/sonar/core/plugins/DefaultPluginMetadataTest.java @@ -19,15 +19,15 @@ */ package org.sonar.core.plugins; -import org.hamcrest.core.Is; import org.junit.Test; import org.sonar.api.platform.PluginMetadata; import java.io.File; import java.util.Arrays; +import java.util.List; -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.assertThat; +import static com.google.common.collect.Ordering.natural; +import static org.fest.assertions.Assertions.assertThat; public class DefaultPluginMetadataTest { @@ -41,19 +41,25 @@ public class DefaultPluginMetadataTest { .setMainClass("org.Main") .setOrganization("SonarSource") .setOrganizationUrl("http://sonarsource.org") - .setVersion("1.1"); - - assertThat(metadata.getKey(), Is.is("checkstyle")); - assertThat(metadata.getLicense(), Is.is("LGPL")); - assertThat(metadata.getDescription(), Is.is("description")); - assertThat(metadata.getHomepage(), Is.is("http://home")); - assertThat(metadata.getMainClass(), Is.is("org.Main")); - assertThat(metadata.getOrganization(), Is.is("SonarSource")); - assertThat(metadata.getOrganizationUrl(), Is.is("http://sonarsource.org")); - assertThat(metadata.getVersion(), Is.is("1.1")); - assertThat(metadata.getBasePlugin(), nullValue()); - assertThat(metadata.getFile(), not(nullValue())); - assertThat(metadata.getDeployedFiles().size(), is(0)); + .setVersion("1.1") + .setSonarVersion("3.0") + .setUseChildFirstClassLoader(true) + .setCore(false); + + assertThat(metadata.getKey()).isEqualTo("checkstyle"); + assertThat(metadata.getLicense()).isEqualTo("LGPL"); + assertThat(metadata.getDescription()).isEqualTo("description"); + assertThat(metadata.getHomepage()).isEqualTo("http://home"); + assertThat(metadata.getMainClass()).isEqualTo("org.Main"); + assertThat(metadata.getOrganization()).isEqualTo("SonarSource"); + assertThat(metadata.getOrganizationUrl()).isEqualTo("http://sonarsource.org"); + assertThat(metadata.getVersion()).isEqualTo("1.1"); + assertThat(metadata.getSonarVersion()).isEqualTo("3.0"); + assertThat(metadata.isUseChildFirstClassLoader()).isTrue(); + assertThat(metadata.isCore()).isFalse(); + assertThat(metadata.getBasePlugin()).isNull(); + assertThat(metadata.getFile()).isNotNull(); + assertThat(metadata.getDeployedFiles()).isEmpty(); } @Test @@ -61,16 +67,16 @@ public class DefaultPluginMetadataTest { DefaultPluginMetadata metadata = DefaultPluginMetadata.create(new File("sonar-checkstyle-plugin.jar")) .addDeployedFile(new File("foo.jar")) .addDeployedFile(new File("bar.jar")); - assertThat(metadata.getDeployedFiles().size(), is(2)); + + assertThat(metadata.getDeployedFiles()).hasSize(2); } @Test public void testInternalPathToDependencies() { DefaultPluginMetadata metadata = DefaultPluginMetadata.create(new File("sonar-checkstyle-plugin.jar")) - .setPathsToInternalDeps(new String[]{"META-INF/lib/commons-lang.jar", "META-INF/lib/commons-io.jar"}); - assertThat(metadata.getPathsToInternalDeps().length, is(2)); - assertThat(metadata.getPathsToInternalDeps()[0], is("META-INF/lib/commons-lang.jar")); - assertThat(metadata.getPathsToInternalDeps()[1], is("META-INF/lib/commons-io.jar")); + .setPathsToInternalDeps(new String[] {"META-INF/lib/commons-lang.jar", "META-INF/lib/commons-io.jar"}); + + assertThat(metadata.getPathsToInternalDeps()).containsOnly("META-INF/lib/commons-lang.jar", "META-INF/lib/commons-io.jar"); } @Test @@ -78,23 +84,49 @@ public class DefaultPluginMetadataTest { DefaultPluginMetadata checkstyle = DefaultPluginMetadata.create(new File("sonar-checkstyle-plugin.jar")).setKey("checkstyle"); PluginMetadata pmd = DefaultPluginMetadata.create(new File("sonar-pmd-plugin.jar")).setKey("pmd"); - assertThat(checkstyle.equals(pmd), is(false)); - assertThat(checkstyle.equals(checkstyle), is(true)); - assertThat(checkstyle.equals(DefaultPluginMetadata.create(new File("sonar-checkstyle-plugin.jar")).setKey("checkstyle")), is(true)); + assertThat(checkstyle).isEqualTo(checkstyle); + assertThat(checkstyle).isEqualTo(DefaultPluginMetadata.create(new File("sonar-checkstyle-plugin.jar")).setKey("checkstyle")); + assertThat(checkstyle).isNotEqualTo(pmd); } @Test public void shouldCompare() { - PluginMetadata checkstyle = DefaultPluginMetadata.create(new File("sonar-checkstyle-plugin.jar")) + DefaultPluginMetadata checkstyle = DefaultPluginMetadata.create(new File("sonar-checkstyle-plugin.jar")) .setKey("checkstyle") .setName("Checkstyle"); - PluginMetadata pmd = DefaultPluginMetadata.create(new File("sonar-pmd-plugin.jar")) + DefaultPluginMetadata pmd = DefaultPluginMetadata.create(new File("sonar-pmd-plugin.jar")) .setKey("pmd") .setName("PMD"); + List plugins = Arrays.asList(pmd, checkstyle); + + assertThat(natural().sortedCopy(plugins)).onProperty("key").containsExactly("checkstyle", "pmd"); + } + + @Test + public void should_check_compatibility_with_sonar_version() { + assertThat(pluginWithVersion("1.1").isCompatibleWith("1.1")).isTrue(); + assertThat(pluginWithVersion("1.1").isCompatibleWith("1.1.0")).isTrue(); + assertThat(pluginWithVersion("1.0").isCompatibleWith("1.0.0")).isTrue(); + + assertThat(pluginWithVersion("1.0").isCompatibleWith("1.1")).isTrue(); + assertThat(pluginWithVersion("1.1.1").isCompatibleWith("1.1.2")).isTrue(); + assertThat(pluginWithVersion("2.0").isCompatibleWith("2.1.0")).isTrue(); + + assertThat(pluginWithVersion("1.1").isCompatibleWith("1.0")).isFalse(); + assertThat(pluginWithVersion("2.0.1").isCompatibleWith("2.0.0")).isFalse(); + assertThat(pluginWithVersion("2.10").isCompatibleWith("2.1")).isFalse(); + assertThat(pluginWithVersion("10.10").isCompatibleWith("2.2")).isFalse(); + + assertThat(pluginWithVersion("1.1-SNAPSHOT").isCompatibleWith("1.0")).isFalse(); + assertThat(pluginWithVersion("1.1-SNAPSHOT").isCompatibleWith("1.1")).isTrue(); + assertThat(pluginWithVersion("1.1-SNAPSHOT").isCompatibleWith("1.2")).isTrue(); + assertThat(pluginWithVersion("1.0.1-SNAPSHOT").isCompatibleWith("1.0")).isFalse(); + + assertThat(pluginWithVersion(null).isCompatibleWith("0")).isTrue(); + assertThat(pluginWithVersion(null).isCompatibleWith("3.1")).isTrue(); + } - PluginMetadata[] array = {pmd, checkstyle}; - Arrays.sort(array); - assertThat(array[0].getKey(), is("checkstyle")); - assertThat(array[1].getKey(), is("pmd")); + static DefaultPluginMetadata pluginWithVersion(String version) { + return DefaultPluginMetadata.create(null).setSonarVersion(version); } } diff --git a/sonar-core/src/test/java/org/sonar/core/plugins/PluginInstallerTest.java b/sonar-core/src/test/java/org/sonar/core/plugins/PluginInstallerTest.java index 9afc9c9de17..e9a893c48be 100644 --- a/sonar-core/src/test/java/org/sonar/core/plugins/PluginInstallerTest.java +++ b/sonar-core/src/test/java/org/sonar/core/plugins/PluginInstallerTest.java @@ -20,86 +20,95 @@ package org.sonar.core.plugins; import org.apache.commons.io.FileUtils; +import org.junit.ClassRule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import java.io.File; import java.io.IOException; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertThat; +import static org.fest.assertions.Assertions.assertThat; public class PluginInstallerTest { - private PluginInstaller extractor= new PluginInstaller(); + private PluginInstaller extractor = new PluginInstaller(); + + @ClassRule + public static TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test public void shouldExtractMetadata() { DefaultPluginMetadata metadata = extractor.extractMetadata(getFile("sonar-checkstyle-plugin-2.8.jar"), true); - assertThat(metadata.getKey(), is("checkstyle")); - assertThat(metadata.getBasePlugin(), nullValue()); - assertThat(metadata.getName(), is("Checkstyle")); - assertThat(metadata.isCore(), is(true)); - assertThat(metadata.getFile().getName(), is("sonar-checkstyle-plugin-2.8.jar")); + + assertThat(metadata.getKey()).isEqualTo("checkstyle"); + assertThat(metadata.getBasePlugin()).isNull(); + assertThat(metadata.getName()).isEqualTo("Checkstyle"); + assertThat(metadata.isCore()).isEqualTo(true); + assertThat(metadata.getFile().getName()).isEqualTo("sonar-checkstyle-plugin-2.8.jar"); + assertThat(metadata.getVersion()).isEqualTo("2.8"); + } + + @Test + public void should_read_sonar_version() { + DefaultPluginMetadata metadata = extractor.extractMetadata(getFile("sonar-switch-off-violations-plugin-1.1.jar"), false); + + assertThat(metadata.getVersion()).isEqualTo("1.1"); + assertThat(metadata.getSonarVersion()).isEqualTo("2.5"); } @Test public void shouldExtractDeprecatedMetadata() { DefaultPluginMetadata metadata = extractor.extractMetadata(getFile("sonar-emma-plugin-0.3.jar"), false); - assertThat(metadata.getKey(), is("emma")); - assertThat(metadata.getBasePlugin(), nullValue()); - assertThat(metadata.getName(), is("Emma")); + + assertThat(metadata.getKey()).isEqualTo("emma"); + assertThat(metadata.getBasePlugin()).isNull(); + assertThat(metadata.getName()).isEqualTo("Emma"); } @Test public void shouldExtractExtensionMetadata() { DefaultPluginMetadata metadata = extractor.extractMetadata(getFile("sonar-checkstyle-extensions-plugin-0.1-SNAPSHOT.jar"), true); - assertThat(metadata.getKey(), is("checkstyleextensions")); - assertThat(metadata.getBasePlugin(), is("checkstyle")); + + assertThat(metadata.getKey()).isEqualTo("checkstyleextensions"); + assertThat(metadata.getBasePlugin()).isEqualTo("checkstyle"); } @Test public void shouldCopyAndExtractDependencies() throws IOException { - File toDir = new File("target/test-tmp/PluginInstallerTest/shouldCopyAndExtractDependencies"); - FileUtils.forceMkdir(toDir); - FileUtils.cleanDirectory(toDir); + File toDir = temporaryFolder.newFolder(); DefaultPluginMetadata metadata = extractor.install(getFile("sonar-checkstyle-plugin-2.8.jar"), true, null, toDir); - assertThat(metadata.getKey(), is("checkstyle")); - assertThat(new File(toDir, "sonar-checkstyle-plugin-2.8.jar").exists(), is(true)); - assertThat(new File(toDir, "META-INF/lib/checkstyle-5.1.jar").exists(), is(true)); + assertThat(metadata.getKey()).isEqualTo("checkstyle"); + assertThat(new File(toDir, "sonar-checkstyle-plugin-2.8.jar")).exists(); + assertThat(new File(toDir, "META-INF/lib/checkstyle-5.1.jar")).exists(); } @Test public void shouldExtractOnlyDependencies() throws IOException { - File toDir = new File("target/test-tmp/PluginInstallerTest/shouldExtractOnlyDependencies"); - FileUtils.forceMkdir(toDir); - FileUtils.cleanDirectory(toDir); + File toDir = temporaryFolder.newFolder(); extractor.install(getFile("sonar-checkstyle-plugin-2.8.jar"), true, null, toDir); - assertThat(new File(toDir, "sonar-checkstyle-plugin-2.8.jar").exists(), is(true)); - assertThat(new File(toDir, "META-INF/MANIFEST.MF").exists(), is(false)); - assertThat(new File(toDir, "org/sonar/plugins/checkstyle/CheckstyleVersion.class").exists(), is(false)); + assertThat(new File(toDir, "sonar-checkstyle-plugin-2.8.jar")).exists(); + assertThat(new File(toDir, "META-INF/MANIFEST.MF")).doesNotExist(); + assertThat(new File(toDir, "org/sonar/plugins/checkstyle/CheckstyleVersion.class")).doesNotExist(); } @Test public void shouldCopyRuleExtensionsOnServerSide() throws IOException { - File toDir = new File("target/test-tmp/PluginInstallerTest/shouldCopyRuleExtensionsOnServerSide"); - FileUtils.forceMkdir(toDir); - FileUtils.cleanDirectory(toDir); + File toDir = temporaryFolder.newFolder(); DefaultPluginMetadata metadata = DefaultPluginMetadata.create(getFile("sonar-checkstyle-plugin-2.8.jar")) .setKey("checkstyle") - .addDeprecatedExtension(getFile("PluginInstallerTest/shouldCopyRuleExtensionsOnServerSide/checkstyle-extension.xml")); + .addDeprecatedExtension(getFile("checkstyle-extension.xml")); extractor.install(metadata, toDir); - assertThat(new File(toDir, "sonar-checkstyle-plugin-2.8.jar").exists(), is(true)); - assertThat(new File(toDir, "checkstyle-extension.xml").exists(), is(true)); + assertThat(new File(toDir, "sonar-checkstyle-plugin-2.8.jar")).exists(); + assertThat(new File(toDir, "checkstyle-extension.xml")).exists(); } - private File getFile(String filename) { - return FileUtils.toFile(getClass().getResource("/org/sonar/core/plugins/" + filename)); + static File getFile(String filename) { + return FileUtils.toFile(PluginInstallerTest.class.getResource("/org/sonar/core/plugins/" + filename)); } } diff --git a/sonar-core/src/test/resources/org/sonar/core/plugins/PluginInstallerTest/shouldCopyRuleExtensionsOnServerSide/checkstyle-extension.xml b/sonar-core/src/test/resources/org/sonar/core/plugins/checkstyle-extension.xml similarity index 100% rename from sonar-core/src/test/resources/org/sonar/core/plugins/PluginInstallerTest/shouldCopyRuleExtensionsOnServerSide/checkstyle-extension.xml rename to sonar-core/src/test/resources/org/sonar/core/plugins/checkstyle-extension.xml diff --git a/sonar-core/src/test/resources/org/sonar/core/plugins/sonar-switch-off-violations-plugin-1.1.jar b/sonar-core/src/test/resources/org/sonar/core/plugins/sonar-switch-off-violations-plugin-1.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..8044dff8988779308c809c2e64c45afb8ee5430b GIT binary patch literal 12689 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 N{Scqt%qf3-{U5R(SC;?) literal 0 HcmV?d00001 diff --git a/sonar-server/src/main/java/org/sonar/server/plugins/PluginDeployer.java b/sonar-server/src/main/java/org/sonar/server/plugins/PluginDeployer.java index 79618d14160..775408ca64c 100644 --- a/sonar-server/src/main/java/org/sonar/server/plugins/PluginDeployer.java +++ b/sonar-server/src/main/java/org/sonar/server/plugins/PluginDeployer.java @@ -19,6 +19,7 @@ */ package org.sonar.server.plugins; +import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.apache.commons.io.FileUtils; @@ -27,7 +28,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.ServerComponent; import org.sonar.api.platform.PluginMetadata; -import org.sonar.api.utils.Logs; +import org.sonar.api.platform.Server; import org.sonar.api.utils.SonarException; import org.sonar.api.utils.TimeProfiler; import org.sonar.core.plugins.DefaultPluginMetadata; @@ -45,15 +46,17 @@ public class PluginDeployer implements ServerComponent { private static final Logger LOG = LoggerFactory.getLogger(PluginDeployer.class); + private final Server server; private final DefaultServerFileSystem fileSystem; - private final Map pluginByKeys = Maps.newHashMap(); private final PluginInstaller installer; + private final Map pluginByKeys = Maps.newHashMap(); - public PluginDeployer(DefaultServerFileSystem fileSystem) { - this(fileSystem, new PluginInstaller()); + public PluginDeployer(Server server, DefaultServerFileSystem fileSystem) { + this(server, fileSystem, new PluginInstaller()); } - PluginDeployer(DefaultServerFileSystem fileSystem, PluginInstaller installer) { + PluginDeployer(Server server, DefaultServerFileSystem fileSystem, PluginInstaller installer) { + this.server = server; this.fileSystem = fileSystem; this.installer = installer; } @@ -91,19 +94,20 @@ public class PluginDeployer implements ServerComponent { private void registerPlugin(File file, boolean isCore, boolean canDelete) { DefaultPluginMetadata metadata = installer.extractMetadata(file, isCore); - if (StringUtils.isNotBlank(metadata.getKey())) { - PluginMetadata existing = pluginByKeys.get(metadata.getKey()); - if (existing != null) { - if (canDelete) { - FileUtils.deleteQuietly(existing.getFile()); - Logs.INFO.info("Plugin " + metadata.getKey() + " replaced by new version"); - - } else { - throw new ServerStartException("Found two plugins with the same key '" + metadata.getKey() + "': " + metadata.getFile().getName() + " and " - + existing.getFile().getName()); - } - } - pluginByKeys.put(metadata.getKey(), metadata); + if (StringUtils.isBlank(metadata.getKey())) { + return; + } + + PluginMetadata existing = pluginByKeys.put(metadata.getKey(), metadata); + + if ((existing != null) && !canDelete) { + throw new ServerStartException("Found two plugins with the same key '" + metadata.getKey() + "': " + metadata.getFile().getName() + " and " + + existing.getFile().getName()); + } + + if (existing != null) { + FileUtils.deleteQuietly(existing.getFile()); + LOG.info("Plugin " + metadata.getKey() + " replaced by new version"); } } @@ -186,9 +190,13 @@ public class PluginDeployer implements ServerComponent { } private void deploy(DefaultPluginMetadata plugin) { - try { - LOG.debug("Deploy plugin " + plugin); + LOG.debug("Deploy plugin " + plugin); + Preconditions.checkState(plugin.isCompatibleWith(server.getVersion()), + "Plugin %s needs a more recent version of Sonar than %s. At least %s is expected", + plugin.getKey(), server.getVersion(), plugin.getSonarVersion()); + + try { File pluginDeployDir = new File(fileSystem.getDeployedPluginsDir(), plugin.getKey()); FileUtils.forceMkdir(pluginDeployDir); FileUtils.cleanDirectory(pluginDeployDir); @@ -199,7 +207,6 @@ public class PluginDeployer implements ServerComponent { } installer.install(plugin, pluginDeployDir); - } catch (IOException e) { throw new RuntimeException("Fail to deploy the plugin " + plugin, e); } diff --git a/sonar-server/src/test/java/org/sonar/server/plugins/PluginDeployerTest.java b/sonar-server/src/test/java/org/sonar/server/plugins/PluginDeployerTest.java index 6adefb336f9..d134a23ce07 100644 --- a/sonar-server/src/test/java/org/sonar/server/plugins/PluginDeployerTest.java +++ b/sonar-server/src/test/java/org/sonar/server/plugins/PluginDeployerTest.java @@ -19,12 +19,13 @@ */ package org.sonar.server.plugins; -import org.hamcrest.core.Is; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.rules.TestName; import org.sonar.api.platform.PluginMetadata; +import org.sonar.api.platform.Server; import org.sonar.core.plugins.PluginInstaller; import org.sonar.server.platform.DefaultServerFileSystem; import org.sonar.server.platform.ServerStartException; @@ -32,8 +33,9 @@ import org.sonar.test.TestUtils; import java.io.File; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertThat; +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class PluginDeployerTest { @@ -42,17 +44,22 @@ public class PluginDeployerTest { private File homeDir; private File deployDir; private PluginDeployer deployer; + private Server server = mock(Server.class); @Rule public TestName name = new TestName(); + @Rule + public ExpectedException exception = ExpectedException.none(); + @Before public void start() { + when(server.getVersion()).thenReturn("3.1"); homeDir = TestUtils.getResource(PluginDeployerTest.class, name.getMethodName()); deployDir = TestUtils.getTestTempDir(PluginDeployerTest.class, name.getMethodName() + "/deploy"); fileSystem = new DefaultServerFileSystem(null, homeDir, deployDir); extractor = new PluginInstaller(); - deployer = new PluginDeployer(fileSystem, extractor); + deployer = new PluginDeployer(server, fileSystem, extractor); } @Test @@ -60,18 +67,18 @@ public class PluginDeployerTest { deployer.start(); // check that the plugin is registered - assertThat(deployer.getMetadata().size(), Is.is(1)); // no more checkstyle + assertThat(deployer.getMetadata()).hasSize(1); // no more checkstyle PluginMetadata plugin = deployer.getMetadata("foo"); - assertThat(plugin.getName(), is("Foo")); - assertThat(plugin.getDeployedFiles().size(), is(1)); - assertThat(plugin.isCore(), is(false)); - assertThat(plugin.isUseChildFirstClassLoader(), is(false)); + assertThat(plugin.getName()).isEqualTo("Foo"); + assertThat(plugin.getDeployedFiles()).hasSize(1); + assertThat(plugin.isCore()).isFalse(); + assertThat(plugin.isUseChildFirstClassLoader()).isFalse(); // check that the file is deployed File deployedJar = new File(deployDir, "plugins/foo/foo-plugin.jar"); - assertThat(deployedJar.exists(), is(true)); - assertThat(deployedJar.isFile(), is(true)); + assertThat(deployedJar.exists()).isTrue(); + assertThat(deployedJar.isFile()).isTrue(); } @Test @@ -79,16 +86,16 @@ public class PluginDeployerTest { deployer.start(); // check that the plugin is registered - assertThat(deployer.getMetadata().size(), Is.is(1)); // no more checkstyle + assertThat(deployer.getMetadata()).hasSize(1); // no more checkstyle PluginMetadata plugin = deployer.getMetadata("buildbreaker"); - assertThat(plugin.isCore(), is(false)); - assertThat(plugin.isUseChildFirstClassLoader(), is(false)); + assertThat(plugin.isCore()).isFalse(); + assertThat(plugin.isUseChildFirstClassLoader()).isFalse(); // check that the file is deployed File deployedJar = new File(deployDir, "plugins/buildbreaker/sonar-build-breaker-plugin-0.1.jar"); - assertThat(deployedJar.exists(), is(true)); - assertThat(deployedJar.isFile(), is(true)); + assertThat(deployedJar.exists()).isTrue(); + assertThat(deployedJar.isFile()).isTrue(); } @Test @@ -96,24 +103,34 @@ public class PluginDeployerTest { deployer.start(); // check that the plugin is registered - assertThat(deployer.getMetadata().size(), Is.is(1)); // no more checkstyle + assertThat(deployer.getMetadata()).hasSize(1); // no more checkstyle PluginMetadata plugin = deployer.getMetadata("foo"); - assertThat(plugin.getDeployedFiles().size(), is(2)); + assertThat(plugin.getDeployedFiles()).hasSize(2); File extFile = plugin.getDeployedFiles().get(1); - assertThat(extFile.getName(), is("foo-extension.txt")); + assertThat(extFile.getName()).isEqualTo("foo-extension.txt"); // check that the extension file is deployed File deployedJar = new File(deployDir, "plugins/foo/foo-extension.txt"); - assertThat(deployedJar.exists(), is(true)); - assertThat(deployedJar.isFile(), is(true)); + assertThat(deployedJar.exists()).isTrue(); + assertThat(deployedJar.isFile()).isTrue(); } @Test public void ignoreJarsWhichAreNotPlugins() { deployer.start(); - assertThat(deployer.getMetadata().size(), Is.is(0)); + assertThat(deployer.getMetadata()).isEmpty(); + } + + @Test + public void should_fail_on_plugin_depending_on_more_recent_sonar() { + when(server.getVersion()).thenReturn("2.0"); + + exception.expect(IllegalStateException.class); + exception.expectMessage("Plugin switchoffviolations needs a more recent version of Sonar than 2.0. At least 2.5 is expected"); + + deployer.start(); } @Test(expected = ServerStartException.class) @@ -125,5 +142,4 @@ public class PluginDeployerTest { public void failIfTwoDeprecatedPluginsWithSameKey() { deployer.start(); } - } diff --git a/sonar-server/src/test/resources/org/sonar/server/plugins/PluginDeployerTest/should_fail_on_plugin_depending_on_more_recent_sonar/extensions/plugins/sonar-switch-off-violations-plugin-1.1.jar b/sonar-server/src/test/resources/org/sonar/server/plugins/PluginDeployerTest/should_fail_on_plugin_depending_on_more_recent_sonar/extensions/plugins/sonar-switch-off-violations-plugin-1.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..8044dff8988779308c809c2e64c45afb8ee5430b GIT binary patch literal 12689 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 N{Scqt%qf3-{U5R(SC;?) literal 0 HcmV?d00001 -- 2.39.5