/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info 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.core.platform;
import com.google.common.collect.ImmutableMap;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.assertj.core.data.MapEntry;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.sonar.api.Plugin;
import org.sonar.api.SonarPlugin;
import org.sonar.updatecenter.common.Version;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
public class PluginLoaderTest {
@Rule
public TemporaryFolder temp = new TemporaryFolder();
PluginClassloaderFactory classloaderFactory = mock(PluginClassloaderFactory.class);
PluginLoader loader = new PluginLoader(new FakePluginExploder(), classloaderFactory);
@Test
public void instantiate_plugin_entry_point() {
PluginClassLoaderDef def = new PluginClassLoaderDef("fake");
def.addMainClass("fake", FakePlugin.class.getName());
Map<String, Plugin> instances = loader.instantiatePluginClasses(ImmutableMap.of(def, getClass().getClassLoader()));
assertThat(instances).containsOnlyKeys("fake");
assertThat(instances.get("fake")).isInstanceOf(FakePlugin.class);
}
@Test
public void plugin_entry_point_must_be_no_arg_public() {
PluginClassLoaderDef def = new PluginClassLoaderDef("fake");
def.addMainClass("fake", IncorrectPlugin.class.getName());
try {
loader.instantiatePluginClasses(ImmutableMap.of(def, getClass().getClassLoader()));
fail();
} catch (IllegalStateException e) {
assertThat(e).hasMessage("Fail to instantiate class [org.sonar.core.platform.PluginLoaderTest$IncorrectPlugin] of plugin [fake]");
}
}
@Test
public void define_classloader() throws Exception {
File jarFile = temp.newFile();
PluginInfo info = new PluginInfo("foo")
.setJarFile(jarFile)
.setMainClass("org.foo.FooPlugin")
.setMinimalSqVersion(Version.create("5.2"));
Collection<PluginClassLoaderDef> defs = loader.defineClassloaders(ImmutableMap.of("foo", info));
assertThat(defs).hasSize(1);
PluginClassLoaderDef def = defs.iterator().next();
assertThat(def.getBasePluginKey()).isEqualTo("foo");
assertThat(def.isSelfFirstStrategy()).isFalse();
assertThat(def.getFiles()).containsOnly(jarFile);
assertThat(def.getMainClassesByPluginKey()).containsOnly(MapEntry.entry("foo", "org.foo.FooPlugin"));
// TODO test mask - require change in sonar-classloader
// built with SQ 5.2+ -> does not need API compatibility mode
assertThat(def.isCompatibilityMode()).isFalse();
}
@Test
public void enable_compatibility_mode_if_plugin_is_built_before_5_2() throws Exception {
File jarFile = temp.newFile();
PluginInfo info = new PluginInfo("foo")
.setJarFile(jarFile)
.setMainClass("org.foo.FooPlugin")
.setMinimalSqVersion(Version.create("4.5.2"));
Collection<PluginClassLoaderDef> defs = loader.defineClassloaders(ImmutableMap.of("foo", info));
assertThat(defs.iterator().next().isCompatibilityMode()).isTrue();
}
/**
* A plugin (the "base" plugin) can be extended by other plugins. In this case they share the same classloader.
*/
@Test
public void test_plugins_sharing_the_same_classloader() throws Exception {
File baseJarFile = temp.newFile();
File extensionJar1 = temp.newFile();
File extensionJar2 = temp.newFile();
PluginInfo base = new PluginInfo("foo")
.setJarFile(baseJarFile)
.setMainClass("org.foo.FooPlugin")
.setUseChildFirstClassLoader(false);
PluginInfo extension1 = new PluginInfo("fooExtension1")
.setJarFile(extensionJar1)
.setMainClass("org.foo.Extension1Plugin")
.setBasePlugin("foo");
// This extension tries to change the classloader-ordering strategy of base plugin
// (see setUseChildFirstClassLoader(true)).
// That is not allowed and should be ignored -> strategy is still the one
// defined on base plugin (parent-first in this example)
PluginInfo extension2 = new PluginInfo("fooExtension2")
.setJarFile(extensionJar2)
.setMainClass("org.foo.Extension2Plugin")
.setBasePlugin("foo")
.setUseChildFirstClassLoader(true);
Collection<PluginClassLoaderDef> defs = loader.defineClassloaders(ImmutableMap.of(
base.getKey(), base, extension1.getKey(), extension1, extension2.getKey(), extension2));
assertThat(defs).hasSize(1);
PluginClassLoaderDef def = defs.iterator().next();
assertThat(def.getBasePluginKey()).isEqualTo("foo");
assertThat(def.isSelfFirstStrategy()).isFalse();
assertThat(def.getFiles()).containsOnly(baseJarFile, extensionJar1, extensionJar2);
assertThat(def.getMainClassesByPluginKey()).containsOnly(
entry("foo", "org.foo.FooPlugin"),
entry("fooExtension1", "org.foo.Extension1Plugin"),
entry("fooExtension2", "org.foo.Extension2Plugin"));
// TODO test mask - require change in sonar-classloader
}
@Test
public void plugin_is_recognised_as_priviledge_if_key_is_views_and_extends_no_other_plugin_and_runs_in_compatibility_mode() throws IOException {
PluginInfo views = create52PluginInfo("views");
Collection<PluginClassLoaderDef> defs = loader.defineClassloaders(ImmutableMap.of("views", views));
assertThat(defs.iterator().next().isPrivileged()).isTrue();
}
@Test
public void plugin_is_recognised_as_priviledge_if_key_is_devcockpit_and_extends_no_other_plugin_and_runs_in_compatibility_mode() throws IOException {
PluginInfo views = create52PluginInfo("devcockpit");
Collection<PluginClassLoaderDef> defs = loader.defineClassloaders(ImmutableMap.of("views", views));
assertThat(defs.iterator().next().isPrivileged()).isTrue();
}
@Test
public void plugin_is_not_recognised_as_system_extension_if_key_is_views_and_extends_another_plugin() throws IOException {
PluginInfo foo = create52PluginInfo("foo");
PluginInfo views = create52PluginInfo("views")
.setBasePlugin("foo");
Collection<PluginClassLoaderDef> defs = loader.defineClassloaders(ImmutableMap.of("foo", foo, "views", views));
assertThat(defs).extracting("compatibilityMode").containsOnly(false, false);
}
private PluginInfo create52PluginInfo(String pluginKey) throws IOException {
File jarFile = temp.newFile();
return new PluginInfo(pluginKey)
.setJarFile(jarFile)
.setMainClass("org.foo." + pluginKey + "Plugin")
.setMinimalSqVersion(Version.create("5.2"));
}
/**
* Does not unzip jar file. It directly returns the JAR file defined on PluginInfo.
*/
private static class FakePluginExploder extends PluginJarExploder {
@Override
public ExplodedPlugin explode(PluginInfo info) {
return new ExplodedPlugin(info.getKey(), info.getNonNullJarFile(), Collections.<File>emptyList());
}
}
public static class FakePlugin extends SonarPlugin {
@Override
public List getExtensions() {
return Collections.emptyList();
}
}
/**
* No public empty-param constructor
*/
public static class IncorrectPlugin extends SonarPlugin {
public IncorrectPlugin(String s) {
}
@Override
public List getExtensions() {
return Collections.emptyList();
}
}
}