import java.util.Iterator; | import java.util.Iterator; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.Objects; | |||||
import java.util.Set; | import java.util.Set; | ||||
import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||
protected boolean exactVersionAllowed = false; | protected boolean exactVersionAllowed = false; | ||||
protected VersionManager versionManager; | protected VersionManager versionManager; | ||||
protected ResolveRecoveryStrategy resolveRecoveryStrategy; | |||||
/** | /** | ||||
* The plugins roots are supplied as comma-separated list by {@code System.getProperty("pf4j.pluginsDir", "plugins")}. | * The plugins roots are supplied as comma-separated list by {@code System.getProperty("pf4j.pluginsDir", "plugins")}. | ||||
* @return true if the plugin was unloaded, otherwise false | * @return true if the plugin was unloaded, otherwise false | ||||
*/ | */ | ||||
protected boolean unloadPlugin(String pluginId, boolean unloadDependents) { | protected boolean unloadPlugin(String pluginId, boolean unloadDependents) { | ||||
try { | |||||
if (unloadDependents) { | |||||
List<String> dependents = dependencyResolver.getDependents(pluginId); | |||||
while (!dependents.isEmpty()) { | |||||
String dependent = dependents.remove(0); | |||||
unloadPlugin(dependent, false); | |||||
dependents.addAll(0, dependencyResolver.getDependents(dependent)); | |||||
} | |||||
if (unloadDependents) { | |||||
List<String> dependents = dependencyResolver.getDependents(pluginId); | |||||
while (!dependents.isEmpty()) { | |||||
String dependent = dependents.remove(0); | |||||
unloadPlugin(dependent, false); | |||||
dependents.addAll(0, dependencyResolver.getDependents(dependent)); | |||||
} | } | ||||
PluginWrapper pluginWrapper = getPlugin(pluginId); | |||||
PluginState pluginState; | |||||
try { | |||||
pluginState = stopPlugin(pluginId, false); | |||||
if (PluginState.STARTED == pluginState) { | |||||
return false; | |||||
} | |||||
} | |||||
log.info("Unload plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor())); | |||||
} catch (Exception e) { | |||||
if (pluginWrapper == null) { | |||||
return false; | |||||
} | |||||
pluginState = PluginState.FAILED; | |||||
if (!plugins.containsKey(pluginId)) { | |||||
// nothing to do | |||||
return false; | |||||
} | |||||
PluginWrapper pluginWrapper = getPlugin(pluginId); | |||||
PluginState pluginState; | |||||
try { | |||||
pluginState = stopPlugin(pluginId, false); | |||||
if (PluginState.STARTED == pluginState) { | |||||
return false; | |||||
} | } | ||||
// remove the plugin | |||||
plugins.remove(pluginId); | |||||
getResolvedPlugins().remove(pluginWrapper); | |||||
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState)); | |||||
// remove the classloader | |||||
Map<String, ClassLoader> pluginClassLoaders = getPluginClassLoaders(); | |||||
if (pluginClassLoaders.containsKey(pluginId)) { | |||||
ClassLoader classLoader = pluginClassLoaders.remove(pluginId); | |||||
if (classLoader instanceof Closeable) { | |||||
try { | |||||
((Closeable) classLoader).close(); | |||||
} catch (IOException e) { | |||||
throw new PluginRuntimeException(e, "Cannot close classloader"); | |||||
} | |||||
log.info("Unload plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor())); | |||||
} catch (Exception e) { | |||||
log.error("Cannot stop plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()), e); | |||||
pluginState = PluginState.FAILED; | |||||
} | |||||
// remove the plugin | |||||
plugins.remove(pluginId); | |||||
getResolvedPlugins().remove(pluginWrapper); | |||||
getUnresolvedPlugins().remove(pluginWrapper); | |||||
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState)); | |||||
// remove the classloader | |||||
Map<String, ClassLoader> pluginClassLoaders = getPluginClassLoaders(); | |||||
if (pluginClassLoaders.containsKey(pluginId)) { | |||||
ClassLoader classLoader = pluginClassLoaders.remove(pluginId); | |||||
if (classLoader instanceof Closeable) { | |||||
try { | |||||
((Closeable) classLoader).close(); | |||||
} catch (IOException e) { | |||||
throw new PluginRuntimeException(e, "Cannot close classloader"); | |||||
} | } | ||||
} | } | ||||
return true; | |||||
} catch (IllegalArgumentException e) { | |||||
// ignore not found exceptions because this method is recursive | |||||
} | } | ||||
return false; | |||||
return true; | |||||
} | } | ||||
@Override | @Override | ||||
* Check if the plugin exists in the list of plugins. | * Check if the plugin exists in the list of plugins. | ||||
* | * | ||||
* @param pluginId the pluginId to check | * @param pluginId the pluginId to check | ||||
* @throws IllegalArgumentException if the plugin does not exist | |||||
* @throws PluginNotFoundException if the plugin does not exist | |||||
*/ | */ | ||||
protected void checkPluginId(String pluginId) { | protected void checkPluginId(String pluginId) { | ||||
if (!plugins.containsKey(pluginId)) { | if (!plugins.containsKey(pluginId)) { | ||||
throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId)); | |||||
throw new PluginNotFoundException(pluginId); | |||||
} | } | ||||
} | } | ||||
versionManager = createVersionManager(); | versionManager = createVersionManager(); | ||||
dependencyResolver = new DependencyResolver(versionManager); | dependencyResolver = new DependencyResolver(versionManager); | ||||
resolveRecoveryStrategy = ResolveRecoveryStrategy.THROW_EXCEPTION; | |||||
} | } | ||||
/** | /** | ||||
* @throws PluginRuntimeException if something goes wrong | * @throws PluginRuntimeException if something goes wrong | ||||
*/ | */ | ||||
protected void resolvePlugins() { | protected void resolvePlugins() { | ||||
// retrieves the plugins descriptors | |||||
List<PluginDescriptor> descriptors = plugins.values().stream() | |||||
.map(PluginWrapper::getDescriptor) | |||||
.collect(Collectors.toList()); | |||||
DependencyResolver.Result result = dependencyResolver.resolve(descriptors); | |||||
if (result.hasCyclicDependency()) { | |||||
throw new DependencyResolver.CyclicDependencyException(); | |||||
} | |||||
List<String> notFoundDependencies = result.getNotFoundDependencies(); | |||||
if (!notFoundDependencies.isEmpty()) { | |||||
throw new DependencyResolver.DependenciesNotFoundException(notFoundDependencies); | |||||
} | |||||
List<DependencyResolver.WrongDependencyVersion> wrongVersionDependencies = result.getWrongVersionDependencies(); | |||||
if (!wrongVersionDependencies.isEmpty()) { | |||||
throw new DependencyResolver.DependenciesWrongVersionException(wrongVersionDependencies); | |||||
} | |||||
DependencyResolver.Result result = resolveDependencies(); | |||||
List<String> sortedPlugins = result.getSortedPlugins(); | List<String> sortedPlugins = result.getSortedPlugins(); | ||||
// move plugins from "unresolved" to "resolved" | // move plugins from "unresolved" to "resolved" | ||||
return extensions; | return extensions; | ||||
} | } | ||||
protected DependencyResolver.Result resolveDependencies() { | |||||
// retrieves the plugins descriptors | |||||
List<PluginDescriptor> descriptors = plugins.values().stream() | |||||
.map(PluginWrapper::getDescriptor) | |||||
.collect(Collectors.toList()); | |||||
DependencyResolver.Result result = dependencyResolver.resolve(descriptors); | |||||
if (result.isOK()) { | |||||
return result; | |||||
} | |||||
if (result.hasCyclicDependency()) { | |||||
// cannot recover from cyclic dependency | |||||
throw new DependencyResolver.CyclicDependencyException(); | |||||
} | |||||
List<String> notFoundDependencies = result.getNotFoundDependencies(); | |||||
if (result.hasNotFoundDependencies() && resolveRecoveryStrategy.equals(ResolveRecoveryStrategy.THROW_EXCEPTION)) { | |||||
throw new DependencyResolver.DependenciesNotFoundException(notFoundDependencies); | |||||
} | |||||
List<DependencyResolver.WrongDependencyVersion> wrongVersionDependencies = result.getWrongVersionDependencies(); | |||||
if (result.hasWrongVersionDependencies() && resolveRecoveryStrategy.equals(ResolveRecoveryStrategy.THROW_EXCEPTION)) { | |||||
throw new DependencyResolver.DependenciesWrongVersionException(wrongVersionDependencies); | |||||
} | |||||
List<PluginDescriptor> resolvedDescriptors = new ArrayList<>(descriptors); | |||||
for (String notFoundDependency : notFoundDependencies) { | |||||
List<String> dependents = dependencyResolver.getDependents(notFoundDependency); | |||||
dependents.forEach(dependent -> resolvedDescriptors.removeIf(descriptor -> descriptor.getPluginId().equals(dependent))); | |||||
} | |||||
for (DependencyResolver.WrongDependencyVersion wrongVersionDependency : wrongVersionDependencies) { | |||||
resolvedDescriptors.removeIf(descriptor -> descriptor.getPluginId().equals(wrongVersionDependency.getDependencyId())); | |||||
} | |||||
List<PluginDescriptor> unresolvedDescriptors = new ArrayList<>(descriptors); | |||||
unresolvedDescriptors.removeAll(resolvedDescriptors); | |||||
for (PluginDescriptor unresolvedDescriptor : unresolvedDescriptors) { | |||||
unloadPlugin(unresolvedDescriptor.getPluginId(), false); | |||||
} | |||||
return resolveDependencies(); | |||||
} | |||||
/** | |||||
* Retrieve the strategy for handling the recovery of a plugin resolve (load) failure. | |||||
* Default is {@link ResolveRecoveryStrategy#THROW_EXCEPTION}. | |||||
* | |||||
* @return the strategy | |||||
*/ | |||||
protected final ResolveRecoveryStrategy getResolveRecoveryStrategy() { | |||||
return resolveRecoveryStrategy; | |||||
} | |||||
/** | |||||
* Set the strategy for handling the recovery of a plugin resolve (load) failure. | |||||
* | |||||
* @param resolveRecoveryStrategy the strategy | |||||
*/ | |||||
protected void setResolveRecoveryStrategy(ResolveRecoveryStrategy resolveRecoveryStrategy) { | |||||
Objects.requireNonNull(resolveRecoveryStrategy, "resolveRecoveryStrategy cannot be null"); | |||||
this.resolveRecoveryStrategy = resolveRecoveryStrategy; | |||||
} | |||||
/** | |||||
* Strategy for handling the recovery of a plugin that could not be resolved | |||||
* (loaded) due to a dependency problem. | |||||
*/ | |||||
public enum ResolveRecoveryStrategy { | |||||
/** | |||||
* Throw an exception when a resolve (load) failure occurs. | |||||
*/ | |||||
THROW_EXCEPTION, | |||||
/** | |||||
* Ignore the plugin with the resolve (load) failure and continue. | |||||
* The plugin with problems will be removed/unloaded from the plugins list. | |||||
*/ | |||||
IGNORE_PLUGIN_AND_CONTINUE | |||||
} | |||||
} | } |
return cyclicDependency; | return cyclicDependency; | ||||
} | } | ||||
/** | |||||
* Returns true if there are dependencies required that were not found. | |||||
* | |||||
* @return true if there are dependencies required that were not found | |||||
*/ | |||||
public boolean hasNotFoundDependencies() { | |||||
return !notFoundDependencies.isEmpty(); | |||||
} | |||||
/** | /** | ||||
* Returns a list with dependencies required that were not found. | * Returns a list with dependencies required that were not found. | ||||
* | * | ||||
return notFoundDependencies; | return notFoundDependencies; | ||||
} | } | ||||
/** | |||||
* Returns true if there are dependencies with wrong version. | |||||
* | |||||
* @return true if there are dependencies with wrong version | |||||
*/ | |||||
public boolean hasWrongVersionDependencies() { | |||||
return !wrongVersionDependencies.isEmpty(); | |||||
} | |||||
/** | /** | ||||
* Returns a list with dependencies with wrong version. | * Returns a list with dependencies with wrong version. | ||||
* | * | ||||
return wrongVersionDependencies; | return wrongVersionDependencies; | ||||
} | } | ||||
/** | |||||
* Returns true if the result is OK (no cyclic dependency, no not found dependencies, no wrong version dependencies). | |||||
* | |||||
* @return true if the result is OK | |||||
*/ | |||||
public boolean isOK() { | |||||
return !hasCyclicDependency() && !hasNotFoundDependencies() && !hasWrongVersionDependencies(); | |||||
} | |||||
/** | /** | ||||
* Get the list of plugins in dependency sorted order. | * Get the list of plugins in dependency sorted order. | ||||
* | * | ||||
*/ | */ | ||||
public static class DependenciesWrongVersionException extends PluginRuntimeException { | public static class DependenciesWrongVersionException extends PluginRuntimeException { | ||||
private List<WrongDependencyVersion> dependencies; | |||||
private final List<WrongDependencyVersion> dependencies; | |||||
public DependenciesWrongVersionException(List<WrongDependencyVersion> dependencies) { | public DependenciesWrongVersionException(List<WrongDependencyVersion> dependencies) { | ||||
super("Dependencies '{}' have wrong version", dependencies); | super("Dependencies '{}' have wrong version", dependencies); |
/* | |||||
* Copyright (C) 2012-present the original author or authors. | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
*/ | |||||
package org.pf4j; | |||||
/** | |||||
* Thrown when a plugin is not found. | |||||
* | |||||
* @author Decebal Suiu | |||||
*/ | |||||
public class PluginNotFoundException extends PluginRuntimeException { | |||||
private final String pluginId; | |||||
public PluginNotFoundException(String pluginId) { | |||||
super("Plugin '{}' not found", pluginId); | |||||
this.pluginId = pluginId; | |||||
} | |||||
public String getPluginId() { | |||||
return pluginId; | |||||
} | |||||
} |
import org.junit.jupiter.api.AfterEach; | import org.junit.jupiter.api.AfterEach; | ||||
import org.junit.jupiter.api.BeforeEach; | import org.junit.jupiter.api.BeforeEach; | ||||
import org.junit.jupiter.api.Test; | import org.junit.jupiter.api.Test; | ||||
import org.junit.jupiter.api.io.TempDir; | |||||
import org.pf4j.test.TestExtension; | import org.pf4j.test.TestExtension; | ||||
import org.pf4j.test.TestExtensionPoint; | import org.pf4j.test.TestExtensionPoint; | ||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.nio.file.Path; | |||||
import java.nio.file.Paths; | import java.nio.file.Paths; | ||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.Arrays; | import java.util.Arrays; | ||||
@Test | @Test | ||||
void checkNotExistedPluginId() { | void checkNotExistedPluginId() { | ||||
assertThrows(IllegalArgumentException.class, () -> pluginManager.checkPluginId("plugin1")); | |||||
assertThrows(PluginNotFoundException.class, () -> pluginManager.checkPluginId("plugin1")); | |||||
} | } | ||||
private PluginWrapper createPluginWrapper(String pluginId) { | private PluginWrapper createPluginWrapper(String pluginId) { | ||||
pluginDescriptor.setPluginVersion("1.2.3"); | pluginDescriptor.setPluginVersion("1.2.3"); | ||||
PluginWrapper pluginWrapper = new PluginWrapper(pluginManager, pluginDescriptor, Paths.get("plugin1"), getClass().getClassLoader()); | PluginWrapper pluginWrapper = new PluginWrapper(pluginManager, pluginDescriptor, Paths.get("plugin1"), getClass().getClassLoader()); | ||||
Plugin plugin= mock(Plugin.class); | |||||
Plugin plugin = mock(Plugin.class); | |||||
pluginWrapper.setPluginFactory(wrapper -> plugin); | pluginWrapper.setPluginFactory(wrapper -> plugin); | ||||
return pluginWrapper; | return pluginWrapper; |
import static org.junit.jupiter.api.Assertions.assertEquals; | import static org.junit.jupiter.api.Assertions.assertEquals; | ||||
import static org.junit.jupiter.api.Assertions.assertFalse; | import static org.junit.jupiter.api.Assertions.assertFalse; | ||||
import static org.junit.jupiter.api.Assertions.assertNotEquals; | |||||
import static org.junit.jupiter.api.Assertions.assertNotNull; | |||||
import static org.junit.jupiter.api.Assertions.assertSame; | import static org.junit.jupiter.api.Assertions.assertSame; | ||||
import static org.junit.jupiter.api.Assertions.assertThrows; | import static org.junit.jupiter.api.Assertions.assertThrows; | ||||
import static org.junit.jupiter.api.Assertions.assertTrue; | import static org.junit.jupiter.api.Assertions.assertTrue; | ||||
@Test | @Test | ||||
public void loadedPluginWithMissingDependencyCanBeUnloaded() throws IOException { | public void loadedPluginWithMissingDependencyCanBeUnloaded() throws IOException { | ||||
pluginManager.setResolveRecoveryStrategy(AbstractPluginManager.ResolveRecoveryStrategy.IGNORE_PLUGIN_AND_CONTINUE); | |||||
PluginZip pluginZip = new PluginZip.Builder(pluginsPath.resolve("my-plugin-1.1.1.zip"), "myPlugin") | PluginZip pluginZip = new PluginZip.Builder(pluginsPath.resolve("my-plugin-1.1.1.zip"), "myPlugin") | ||||
.pluginVersion("1.1.1") | .pluginVersion("1.1.1") | ||||
.pluginDependencies("myBasePlugin") | .pluginDependencies("myBasePlugin") | ||||
.build(); | .build(); | ||||
try { | |||||
pluginManager.loadPlugin(pluginZip.path()); | |||||
} catch (DependencyResolver.DependenciesNotFoundException e) { | |||||
// expected | |||||
} | |||||
// try to load the plugin with a missing dependency | |||||
pluginManager.loadPlugin(pluginZip.path()); | |||||
// the plugin is unloaded automatically | |||||
assertTrue(pluginManager.getPlugins().isEmpty()); | |||||
// start plugins | |||||
pluginManager.startPlugins(); | |||||
assertTrue(pluginManager.getStartedPlugins().isEmpty()); | |||||
assertTrue(pluginManager.getUnresolvedPlugins().isEmpty()); | |||||
assertTrue(pluginManager.getResolvedPlugins().isEmpty()); | |||||
assertTrue(pluginManager.getPlugins().isEmpty()); | |||||
} | |||||
@Test | |||||
void loadingPluginWithMissingDependencyDoesNotBreakOtherPlugins() throws IOException { | |||||
pluginManager.setResolveRecoveryStrategy(AbstractPluginManager.ResolveRecoveryStrategy.IGNORE_PLUGIN_AND_CONTINUE); | |||||
// Load 2 plugins, one with a dependency that is missing and one without any dependencies. | |||||
PluginZip pluginZip1 = new PluginZip.Builder(pluginsPath.resolve("my-first-plugin-1.1.1.zip"), "myPlugin1") | |||||
.pluginVersion("1.1.1") | |||||
.pluginDependencies("myBasePlugin") | |||||
.build(); | |||||
PluginZip pluginZip2 = new PluginZip.Builder(pluginsPath.resolve("my-second-plugin-2.2.2.zip"), "myPlugin2") | |||||
.pluginVersion("2.2.2") | |||||
.build(); | |||||
pluginManager.loadPlugins(); | |||||
pluginManager.startPlugins(); | pluginManager.startPlugins(); | ||||
PluginWrapper myPlugin = pluginManager.getPlugin(pluginZip.pluginId()); | |||||
assertNotNull(myPlugin); | |||||
assertNotEquals(PluginState.STARTED, myPlugin.getPluginState()); | |||||
// myPlugin2 should have been started at this point. | |||||
assertEquals(PluginState.STARTED, pluginManager.getPlugin(pluginZip2.pluginId()).getPluginState()); | |||||
pluginManager.unloadPlugin(pluginZip1.pluginId()); | |||||
// No traces should remain of myPlugin1. | |||||
assertTrue( | |||||
pluginManager.getUnresolvedPlugins().stream() | |||||
.noneMatch(pluginWrapper -> pluginWrapper.getPluginId().equals(pluginZip1.pluginId())) | |||||
); | |||||
pluginManager.unloadPlugin(pluginZip2.pluginId()); | |||||
// Load the missing dependency, everything should start working. | |||||
PluginZip pluginZipBase = new PluginZip.Builder(pluginsPath.resolve("my-base-plugin-3.0.0.zip"), "myBasePlugin") | |||||
.pluginVersion("3.0.0") | |||||
.build(); | |||||
pluginManager.loadPlugins(); | |||||
pluginManager.startPlugins(); | |||||
assertEquals(PluginState.STARTED, pluginManager.getPlugin(pluginZip1.pluginId()).getPluginState()); | |||||
assertEquals(PluginState.STARTED, pluginManager.getPlugin(pluginZip2.pluginId()).getPluginState()); | |||||
assertEquals(PluginState.STARTED, pluginManager.getPlugin(pluginZipBase.pluginId()).getPluginState()); | |||||
assertTrue(pluginManager.getUnresolvedPlugins().isEmpty()); | |||||
} | |||||
@Test | |||||
void loadingPluginWithMissingDependencyFails() throws IOException { | |||||
pluginManager.setResolveRecoveryStrategy(AbstractPluginManager.ResolveRecoveryStrategy.THROW_EXCEPTION); | |||||
PluginZip pluginZip = new PluginZip.Builder(pluginsPath.resolve("my-plugin-1.1.1.zip"), "myPlugin") | |||||
.pluginVersion("1.1.1") | |||||
.pluginDependencies("myBasePlugin") | |||||
.build(); | |||||
// try to load the plugin with a missing dependency | |||||
Path pluginPath = pluginZip.path(); | |||||
assertThrows(DependencyResolver.DependenciesNotFoundException.class, () -> pluginManager.loadPlugin(pluginPath)); | |||||
} | |||||
@Test | |||||
void loadingPluginWithWrongDependencyVersionFails() throws IOException { | |||||
pluginManager.setResolveRecoveryStrategy(AbstractPluginManager.ResolveRecoveryStrategy.THROW_EXCEPTION); | |||||
PluginZip pluginZip1 = new PluginZip.Builder(pluginsPath.resolve("my-first-plugin-1.1.1.zip"), "myPlugin1") | |||||
.pluginVersion("1.1.1") | |||||
.pluginDependencies("myPlugin2@3.0.0") | |||||
.build(); | |||||
PluginZip pluginZip2 = new PluginZip.Builder(pluginsPath.resolve("my-second-plugin-2.2.2.zip"), "myPlugin2") | |||||
.pluginVersion("2.2.2") | |||||
.build(); | |||||
// try to load the plugins with cyclic dependencies | |||||
Path pluginPath1 = pluginZip1.path(); | |||||
Path pluginPath2 = pluginZip2.path(); | |||||
assertThrows(DependencyResolver.DependenciesWrongVersionException.class, () -> pluginManager.loadPlugins()); | |||||
} | |||||
@Test | |||||
void loadingPluginsWithCyclicDependenciesFails() throws IOException { | |||||
PluginZip pluginZip1 = new PluginZip.Builder(pluginsPath.resolve("my-first-plugin-1.1.1.zip"), "myPlugin1") | |||||
.pluginVersion("1.1.1") | |||||
.pluginDependencies("myPlugin2") | |||||
.build(); | |||||
PluginZip pluginZip2 = new PluginZip.Builder(pluginsPath.resolve("my-second-plugin-2.2.2.zip"), "myPlugin2") | |||||
.pluginVersion("2.2.2") | |||||
.pluginDependencies("myPlugin1") | |||||
.build(); | |||||
assertTrue(pluginManager.unloadPlugin(pluginZip.pluginId())); | |||||
// try to load the plugins with cyclic dependencies | |||||
Path pluginPath1 = pluginZip1.path(); | |||||
Path pluginPath2 = pluginZip2.path(); | |||||
assertThrows(DependencyResolver.CyclicDependencyException.class, () -> pluginManager.loadPlugins()); | |||||
} | } | ||||
} | } |