Skip to content

Commit

Permalink
Add one test for plugin type to PluginsLoaderTests (elastic#117725)
Browse files Browse the repository at this point in the history
* Add one test for plugin type to PluginsLoaderTests

* Suppress ExtraFs (or PluginsUtils etc could fail with extra0 files)
  • Loading branch information
ldematte authored Dec 12, 2024
1 parent 95315cc commit adddfa2
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ static String toModuleName(String name) {
return result;
}

static final String toPackageName(String className) {
static String toPackageName(String className) {
assert className.endsWith(".") == false;
int index = className.lastIndexOf('.');
if (index == -1) {
Expand All @@ -426,11 +426,11 @@ static final String toPackageName(String className) {
}

@SuppressForbidden(reason = "I need to convert URL's to Paths")
static final Path[] urlsToPaths(Set<URL> urls) {
static Path[] urlsToPaths(Set<URL> urls) {
return urls.stream().map(PluginsLoader::uncheckedToURI).map(PathUtils::get).toArray(Path[]::new);
}

static final URI uncheckedToURI(URL url) {
static URI uncheckedToURI(URL url) {
try {
return url.toURI();
} catch (URISyntaxException e) {
Expand Down
249 changes: 249 additions & 0 deletions server/src/test/java/org/elasticsearch/plugins/PluginsLoaderTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,45 @@

package org.elasticsearch.plugins;

import org.apache.lucene.tests.util.LuceneTestCase;
import org.elasticsearch.Version;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.env.TestEnvironment;
import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;
import org.elasticsearch.plugin.analysis.CharFilterFactory;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.PrivilegedOperations;
import org.elasticsearch.test.compiler.InMemoryJavaCompiler;
import org.elasticsearch.test.jar.JarUtils;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;

import static java.util.Map.entry;
import static org.elasticsearch.test.LambdaMatchers.transformedMatch;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;

@ESTestCase.WithoutSecurityManager
@LuceneTestCase.SuppressFileSystems(value = "ExtrasFS")
public class PluginsLoaderTests extends ESTestCase {

private static final Logger logger = LogManager.getLogger(PluginsLoaderTests.class);

static PluginsLoader newPluginsLoader(Settings settings) {
return PluginsLoader.createPluginsLoader(null, TestEnvironment.newEnvironment(settings).pluginsFile(), false);
}

public void testToModuleName() {
assertThat(PluginsLoader.toModuleName("module.name"), equalTo("module.name"));
assertThat(PluginsLoader.toModuleName("module-name"), equalTo("module.name"));
Expand All @@ -28,4 +61,220 @@ public void testToModuleName() {
assertThat(PluginsLoader.toModuleName("_module_name"), equalTo("_module_name"));
assertThat(PluginsLoader.toModuleName("_"), equalTo("_"));
}

public void testStablePluginLoading() throws Exception {
final Path home = createTempDir();
final Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), home).build();
final Path plugins = home.resolve("plugins");
final Path plugin = plugins.resolve("stable-plugin");
Files.createDirectories(plugin);
PluginTestUtil.writeStablePluginProperties(
plugin,
"description",
"description",
"name",
"stable-plugin",
"version",
"1.0.0",
"elasticsearch.version",
Version.CURRENT.toString(),
"java.version",
System.getProperty("java.specification.version")
);

Path jar = plugin.resolve("impl.jar");
JarUtils.createJarWithEntries(jar, Map.of("p/A.class", InMemoryJavaCompiler.compile("p.A", """
package p;
import java.util.Map;
import org.elasticsearch.plugin.analysis.CharFilterFactory;
import org.elasticsearch.plugin.NamedComponent;
import java.io.Reader;
@NamedComponent( "a_name")
public class A implements CharFilterFactory {
@Override
public Reader create(Reader reader) {
return reader;
}
}
""")));
Path namedComponentFile = plugin.resolve("named_components.json");
Files.writeString(namedComponentFile, """
{
"org.elasticsearch.plugin.analysis.CharFilterFactory": {
"a_name": "p.A"
}
}
""");

var pluginsLoader = newPluginsLoader(settings);
try {
var loadedLayers = pluginsLoader.pluginLayers().toList();

assertThat(loadedLayers, hasSize(1));
assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().getName(), equalTo("stable-plugin"));
assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().isStable(), is(true));

assertThat(pluginsLoader.pluginDescriptors(), hasSize(1));
assertThat(pluginsLoader.pluginDescriptors().get(0).getName(), equalTo("stable-plugin"));
assertThat(pluginsLoader.pluginDescriptors().get(0).isStable(), is(true));

var pluginClassLoader = loadedLayers.get(0).pluginClassLoader();
var pluginModuleLayer = loadedLayers.get(0).pluginModuleLayer();
assertThat(pluginClassLoader, instanceOf(UberModuleClassLoader.class));
assertThat(pluginModuleLayer, is(not(ModuleLayer.boot())));
assertThat(pluginModuleLayer.modules(), contains(transformedMatch(Module::getName, equalTo("synthetic.stable.plugin"))));

if (CharFilterFactory.class.getModule().isNamed() == false) {
// test frameworks run with stable api classes on classpath, so we
// have no choice but to let our class read the unnamed module that
// owns the stable api classes
((UberModuleClassLoader) pluginClassLoader).addReadsSystemClassLoaderUnnamedModule();
}

Class<?> stableClass = pluginClassLoader.loadClass("p.A");
assertThat(stableClass.getModule().getName(), equalTo("synthetic.stable.plugin"));
} finally {
closePluginLoaders(pluginsLoader);
}
}

public void testModularPluginLoading() throws Exception {
final Path home = createTempDir();
final Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), home).build();
final Path plugins = home.resolve("plugins");
final Path plugin = plugins.resolve("modular-plugin");
Files.createDirectories(plugin);
PluginTestUtil.writePluginProperties(
plugin,
"description",
"description",
"name",
"modular-plugin",
"classname",
"p.A",
"modulename",
"modular.plugin",
"version",
"1.0.0",
"elasticsearch.version",
Version.CURRENT.toString(),
"java.version",
System.getProperty("java.specification.version")
);

Path jar = plugin.resolve("impl.jar");
Map<String, CharSequence> sources = Map.ofEntries(entry("module-info", "module modular.plugin { exports p; }"), entry("p.A", """
package p;
import org.elasticsearch.plugins.Plugin;
public class A extends Plugin {
}
"""));

// Usually org.elasticsearch.plugins.Plugin would be in the org.elasticsearch.server module.
// Unfortunately, as tests run non-modular, it will be in the unnamed module, so we need to add a read for it.
var classToBytes = InMemoryJavaCompiler.compile(sources, "--add-reads", "modular.plugin=ALL-UNNAMED");

JarUtils.createJarWithEntries(
jar,
Map.ofEntries(entry("module-info.class", classToBytes.get("module-info")), entry("p/A.class", classToBytes.get("p.A")))
);

var pluginsLoader = newPluginsLoader(settings);
try {
var loadedLayers = pluginsLoader.pluginLayers().toList();

assertThat(loadedLayers, hasSize(1));
assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().getName(), equalTo("modular-plugin"));
assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().isStable(), is(false));
assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().isModular(), is(true));

assertThat(pluginsLoader.pluginDescriptors(), hasSize(1));
assertThat(pluginsLoader.pluginDescriptors().get(0).getName(), equalTo("modular-plugin"));
assertThat(pluginsLoader.pluginDescriptors().get(0).isModular(), is(true));

var pluginModuleLayer = loadedLayers.get(0).pluginModuleLayer();
assertThat(pluginModuleLayer, is(not(ModuleLayer.boot())));
assertThat(pluginModuleLayer.modules(), contains(transformedMatch(Module::getName, equalTo("modular.plugin"))));
} finally {
closePluginLoaders(pluginsLoader);
}
}

public void testNonModularPluginLoading() throws Exception {
final Path home = createTempDir();
final Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), home).build();
final Path plugins = home.resolve("plugins");
final Path plugin = plugins.resolve("non-modular-plugin");
Files.createDirectories(plugin);
PluginTestUtil.writePluginProperties(
plugin,
"description",
"description",
"name",
"non-modular-plugin",
"classname",
"p.A",
"version",
"1.0.0",
"elasticsearch.version",
Version.CURRENT.toString(),
"java.version",
System.getProperty("java.specification.version")
);

Path jar = plugin.resolve("impl.jar");
Map<String, CharSequence> sources = Map.ofEntries(entry("p.A", """
package p;
import org.elasticsearch.plugins.Plugin;
public class A extends Plugin {
}
"""));

var classToBytes = InMemoryJavaCompiler.compile(sources);

JarUtils.createJarWithEntries(jar, Map.ofEntries(entry("p/A.class", classToBytes.get("p.A"))));

var pluginsLoader = newPluginsLoader(settings);
try {
var loadedLayers = pluginsLoader.pluginLayers().toList();

assertThat(loadedLayers, hasSize(1));
assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().getName(), equalTo("non-modular-plugin"));
assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().isStable(), is(false));
assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().isModular(), is(false));

assertThat(pluginsLoader.pluginDescriptors(), hasSize(1));
assertThat(pluginsLoader.pluginDescriptors().get(0).getName(), equalTo("non-modular-plugin"));
assertThat(pluginsLoader.pluginDescriptors().get(0).isModular(), is(false));

var pluginModuleLayer = loadedLayers.get(0).pluginModuleLayer();
assertThat(pluginModuleLayer, is(ModuleLayer.boot()));
} finally {
closePluginLoaders(pluginsLoader);
}
}

// Closes the URLClassLoaders and UberModuleClassloaders created by the given plugin loader.
// We can use the direct ClassLoader from the plugin because tests do not use any parent SPI ClassLoaders.
static void closePluginLoaders(PluginsLoader pluginsLoader) {
pluginsLoader.pluginLayers().forEach(lp -> {
if (lp.pluginClassLoader() instanceof URLClassLoader urlClassLoader) {
try {
PrivilegedOperations.closeURLClassLoader(urlClassLoader);
} catch (IOException unexpected) {
throw new UncheckedIOException(unexpected);
}
} else if (lp.pluginClassLoader() instanceof UberModuleClassLoader loader) {
try {
PrivilegedOperations.closeURLClassLoader(loader.getInternalLoader());
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
logger.info("Cannot close unexpected classloader " + lp.pluginClassLoader());
}
});
}
}

0 comments on commit adddfa2

Please sign in to comment.