mirror of https://github.com/hyperledger/besu
Plugin Framework (#1435)
Provide a mechanism for Pantheon to integrate plugin code at runtime. Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
0b53195b0b
commit
8b7c5f7949
@ -0,0 +1,90 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* 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 tech.pegasys.pantheon.tests.acceptance.plugins; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
import tech.pegasys.pantheon.tests.acceptance.dsl.AcceptanceTestBase; |
||||
import tech.pegasys.pantheon.tests.acceptance.dsl.node.PantheonNode; |
||||
|
||||
import java.io.File; |
||||
import java.io.IOException; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.Path; |
||||
import java.util.Collections; |
||||
import java.util.concurrent.TimeUnit; |
||||
import java.util.stream.Stream; |
||||
|
||||
import org.awaitility.Awaitility; |
||||
import org.junit.Before; |
||||
import org.junit.Ignore; |
||||
import org.junit.Test; |
||||
|
||||
public class PluginsAcceptanceTest extends AcceptanceTestBase { |
||||
private PantheonNode node; |
||||
|
||||
// context: https://en.wikipedia.org/wiki/The_Magic_Words_are_Squeamish_Ossifrage
|
||||
private static final String MAGIC_WORDS = "Squemish Ossifrage"; |
||||
|
||||
@Before |
||||
public void setUp() throws Exception { |
||||
node = |
||||
pantheon.createPluginsNode( |
||||
"node1", |
||||
Collections.singletonList("testPlugin"), |
||||
Collections.singletonList("--Xtest-option=" + MAGIC_WORDS)); |
||||
cluster.start(node); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldRegister() throws IOException { |
||||
final Path registrationFile = node.homeDirectory().resolve("plugins/testPlugin.registered"); |
||||
waitForFile(registrationFile); |
||||
|
||||
// this assert is false as CLI will not be parsed at this point
|
||||
assertThat(Files.readAllLines(registrationFile).stream().anyMatch(s -> s.contains(MAGIC_WORDS))) |
||||
.isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldStart() throws IOException { |
||||
final Path registrationFile = node.homeDirectory().resolve("plugins/testPlugin.started"); |
||||
waitForFile(registrationFile); |
||||
|
||||
// this assert is true as CLI will be parsed at this point
|
||||
assertThat(Files.readAllLines(registrationFile).stream().anyMatch(s -> s.contains(MAGIC_WORDS))) |
||||
.isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
@Ignore("No way to do a graceful shutdown of Pantheon at the moment.") |
||||
public void shouldStop() { |
||||
cluster.stopNode(node); |
||||
waitForFile(node.homeDirectory().resolve("plugins/testPlugin.stopped")); |
||||
} |
||||
|
||||
private void waitForFile(final Path path) { |
||||
final File file = path.toFile(); |
||||
Awaitility.waitAtMost(30, TimeUnit.SECONDS) |
||||
.until( |
||||
() -> { |
||||
if (file.exists()) { |
||||
try (final Stream<String> s = Files.lines(path)) { |
||||
return s.count() > 0; |
||||
} |
||||
} else { |
||||
return false; |
||||
} |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,58 @@ |
||||
/* |
||||
* Copyright 2018 ConsenSys AG. |
||||
* |
||||
* 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. |
||||
*/ |
||||
|
||||
apply plugin: 'java-library' |
||||
|
||||
jar { |
||||
baseName 'pantheon-plugins' |
||||
manifest { |
||||
attributes( |
||||
'Specification-Title': baseName, |
||||
'Specification-Version': project.version, |
||||
'Implementation-Title': baseName, |
||||
'Implementation-Version': calculateVersion() |
||||
) |
||||
} |
||||
} |
||||
|
||||
dependencies { |
||||
implementation 'com.google.guava:guava' |
||||
implementation 'org.apache.logging.log4j:log4j-api' |
||||
|
||||
testAnnotationProcessor 'com.google.auto.service:auto-service' |
||||
|
||||
testImplementation 'com.google.auto.service:auto-service' |
||||
testImplementation 'info.picocli:picocli' |
||||
testImplementation 'junit:junit' |
||||
testImplementation 'org.apache.logging.log4j:log4j-api' |
||||
testImplementation 'org.assertj:assertj-core' |
||||
testImplementation 'org.mockito:mockito-core' |
||||
} |
||||
|
||||
task testJar(type: Jar) { |
||||
archiveName 'testPlugin.jar' |
||||
manifest { |
||||
attributes( |
||||
'Specification-Title': baseName, |
||||
'Specification-Version': project.version, |
||||
'Implementation-Title': baseName, |
||||
'Implementation-Version': calculateVersion() |
||||
) |
||||
} |
||||
archiveClassifier = 'tests' |
||||
from sourceSets.test.output |
||||
} |
||||
|
||||
configurations { testArtifacts } |
||||
|
||||
artifacts { testArtifacts testJar } |
@ -0,0 +1,20 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* 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 tech.pegasys.pantheon.plugins; |
||||
|
||||
import java.util.Optional; |
||||
|
||||
public interface PantheonContext { |
||||
|
||||
<T> Optional<T> getService(Class<T> serviceType); |
||||
} |
@ -0,0 +1,22 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* 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 tech.pegasys.pantheon.plugins; |
||||
|
||||
public interface PantheonPlugin { |
||||
|
||||
void register(PantheonContext context); |
||||
|
||||
void start(); |
||||
|
||||
void stop(); |
||||
} |
@ -0,0 +1,179 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* 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 tech.pegasys.pantheon.plugins.internal; |
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument; |
||||
import static com.google.common.base.Preconditions.checkState; |
||||
|
||||
import tech.pegasys.pantheon.plugins.PantheonContext; |
||||
import tech.pegasys.pantheon.plugins.PantheonPlugin; |
||||
|
||||
import java.io.IOException; |
||||
import java.net.MalformedURLException; |
||||
import java.net.URL; |
||||
import java.net.URLClassLoader; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.Path; |
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.Iterator; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Optional; |
||||
import java.util.ServiceLoader; |
||||
import java.util.stream.Stream; |
||||
|
||||
import com.google.common.annotations.VisibleForTesting; |
||||
import org.apache.logging.log4j.LogManager; |
||||
import org.apache.logging.log4j.Logger; |
||||
|
||||
public class PantheonPluginContextImpl implements PantheonContext { |
||||
|
||||
private static final Logger LOG = LogManager.getLogger(); |
||||
|
||||
private enum Lifecycle { |
||||
UNINITIALIZED, |
||||
REGISTERING, |
||||
REGISTERED, |
||||
STARTING, |
||||
STARTED, |
||||
STOPPING, |
||||
STOPPED |
||||
} |
||||
|
||||
private Lifecycle state = Lifecycle.UNINITIALIZED; |
||||
private final Map<Class<?>, ? super Object> serviceRegistry = new HashMap<>(); |
||||
private final List<PantheonPlugin> plugins = new ArrayList<>(); |
||||
|
||||
public <T> void addService(final Class<T> serviceType, final T service) { |
||||
checkArgument(serviceType.isInterface(), "Services must be Java interfaces."); |
||||
checkArgument( |
||||
serviceType.isInstance(service), |
||||
"The service registered with a type must implement that type"); |
||||
serviceRegistry.put(serviceType, service); |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
@Override |
||||
public <T> Optional<T> getService(final Class<T> serviceType) { |
||||
return Optional.ofNullable((T) serviceRegistry.get(serviceType)); |
||||
} |
||||
|
||||
public void registerPlugins(final Path pluginsDir) { |
||||
checkState( |
||||
state == Lifecycle.UNINITIALIZED, |
||||
"Pantheon plugins have already been registered. Cannot register additional plugins."); |
||||
if (pluginsDir == null) { |
||||
LOG.debug("Plugins are disabled."); |
||||
return; |
||||
} |
||||
state = Lifecycle.REGISTERING; |
||||
|
||||
LOG.debug("Searching for plugins in {}", pluginsDir.toAbsolutePath().toString()); |
||||
try (final Stream<Path> pluginFilesList = Files.list(pluginsDir)) { |
||||
final URL[] pluginJarURLs = |
||||
pluginFilesList |
||||
.filter(p -> p.getFileName().toString().endsWith(".jar")) |
||||
.map(PantheonPluginContextImpl::pathToURIOrNull) |
||||
.toArray(URL[]::new); |
||||
final ServiceLoader<PantheonPlugin> serviceLoader = |
||||
ServiceLoader.load( |
||||
PantheonPlugin.class, |
||||
new URLClassLoader(pluginJarURLs, this.getClass().getClassLoader())); |
||||
|
||||
for (final PantheonPlugin plugin : serviceLoader) { |
||||
try { |
||||
plugin.register(this); |
||||
LOG.debug("Registered plugin of type {}.", plugin.getClass().getName()); |
||||
} catch (final Exception e) { |
||||
LOG.error( |
||||
"Error registering plugin of type {}, start and stop will not be called. \n{}", |
||||
plugin.getClass(), |
||||
e); |
||||
continue; |
||||
} |
||||
plugins.add(plugin); |
||||
} |
||||
|
||||
} catch (final MalformedURLException e) { |
||||
LOG.error("Error converting files to URLs, could not load plugins", e); |
||||
} catch (final IOException e) { |
||||
LOG.error("Error enumerating plugins, could not load plugins", e); |
||||
} |
||||
LOG.debug("Plugin registration complete."); |
||||
state = Lifecycle.REGISTERED; |
||||
} |
||||
|
||||
public void startPlugins() { |
||||
checkState( |
||||
state == Lifecycle.REGISTERED, |
||||
"PantheonContext should be in state %s but it was in %s", |
||||
Lifecycle.REGISTERED, |
||||
state); |
||||
state = Lifecycle.STARTING; |
||||
final Iterator<PantheonPlugin> pluginsIterator = plugins.iterator(); |
||||
|
||||
while (pluginsIterator.hasNext()) { |
||||
final PantheonPlugin plugin = pluginsIterator.next(); |
||||
|
||||
try { |
||||
plugin.start(); |
||||
LOG.debug("Started plugin of type {}.", plugin.getClass().getName()); |
||||
} catch (final Exception e) { |
||||
LOG.error( |
||||
"Error starting plugin of type {}, stop will not be called. \n{}", |
||||
plugin.getClass(), |
||||
e); |
||||
pluginsIterator.remove(); |
||||
} |
||||
} |
||||
|
||||
LOG.debug("Plugin startup complete."); |
||||
state = Lifecycle.STARTED; |
||||
} |
||||
|
||||
public void stopPlugins() { |
||||
checkState( |
||||
state == Lifecycle.STARTED, |
||||
"PantheonContext should be in state %s but it was in %s", |
||||
Lifecycle.STARTED, |
||||
state); |
||||
state = Lifecycle.STOPPING; |
||||
|
||||
for (final PantheonPlugin plugin : plugins) { |
||||
try { |
||||
plugin.stop(); |
||||
LOG.debug("Stopped plugin of type {}.", plugin.getClass().getName()); |
||||
} catch (final Exception e) { |
||||
LOG.error("Error stopping plugin of type {}. \n{}", plugin.getClass(), e); |
||||
} |
||||
} |
||||
|
||||
LOG.debug("Plugin shutdown complete."); |
||||
state = Lifecycle.STOPPED; |
||||
} |
||||
|
||||
private static URL pathToURIOrNull(final Path p) { |
||||
try { |
||||
return p.toUri().toURL(); |
||||
} catch (final MalformedURLException e) { |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
@VisibleForTesting |
||||
List<PantheonPlugin> getPlugins() { |
||||
return Collections.unmodifiableList(plugins); |
||||
} |
||||
} |
@ -0,0 +1,2 @@ |
||||
/** This package will be hidden from external users once Pantheon migrates to Java 11. */ |
||||
package tech.pegasys.pantheon.plugins.internal; |
@ -0,0 +1,34 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* 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 tech.pegasys.pantheon.plugins.services; |
||||
|
||||
import java.util.function.Consumer; |
||||
|
||||
public interface PantheonEvents { |
||||
|
||||
/** |
||||
* Returns the raw RLP of a block that Pantheon has receieved and that has passed basic validation |
||||
* checks. |
||||
* |
||||
* @param blockJSONListener The listener that will accept a JSON string as the event. |
||||
* @return an object to be used as an identifier when de-registering the event. |
||||
*/ |
||||
Object addBlockAddedListener(Consumer<String> blockJSONListener); |
||||
|
||||
/** |
||||
* Remove the blockAdded listener from pantheon notifications. |
||||
* |
||||
* @param listenerIdentifier The instance that was returned from addBlockAddedListener; |
||||
*/ |
||||
void removeBlockAddedObserver(Object listenerIdentifier); |
||||
} |
@ -0,0 +1,27 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* 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 tech.pegasys.pantheon.plugins.services; |
||||
|
||||
/** This service will be available during the registration callbacks. */ |
||||
public interface PicoCLIOptions { |
||||
|
||||
/** |
||||
* During the registration callback plugins can register CLI options that should be added to |
||||
* Pantheon's CLI startup. |
||||
* |
||||
* @param namespace A namespace prefix. All registered options must start with this prefix |
||||
* @param optionObject The instance of the object to be inspected. PicoCLI will reflect the fields |
||||
* of this object to extract the CLI options. |
||||
*/ |
||||
void addPicoCLIOptions(String namespace, Object optionObject); |
||||
} |
@ -0,0 +1,126 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* 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 tech.pegasys.pantheon.plugins; |
||||
|
||||
import tech.pegasys.pantheon.plugins.services.PantheonEvents; |
||||
import tech.pegasys.pantheon.plugins.services.PicoCLIOptions; |
||||
|
||||
import java.io.File; |
||||
import java.io.IOException; |
||||
import java.nio.file.Files; |
||||
import java.util.Collections; |
||||
import java.util.Optional; |
||||
|
||||
import com.google.auto.service.AutoService; |
||||
import org.apache.logging.log4j.LogManager; |
||||
import org.apache.logging.log4j.Logger; |
||||
import picocli.CommandLine.Option; |
||||
|
||||
@AutoService(PantheonPlugin.class) |
||||
public class TestPlugin implements PantheonPlugin { |
||||
private static final Logger LOG = LogManager.getLogger(); |
||||
|
||||
private PantheonContext context; |
||||
private Optional<Object> listenerReference; |
||||
|
||||
@Option(names = "--Xtest-option", hidden = true, defaultValue = "UNSET") |
||||
String testOption = System.getProperty("testPlugin.testOption"); |
||||
|
||||
private String state = "uninited"; |
||||
|
||||
@Override |
||||
public void register(final PantheonContext context) { |
||||
LOG.info("Registring. Test Option is '{}'", testOption); |
||||
state = "registering"; |
||||
|
||||
if ("FAILREGISTER".equals(testOption)) { |
||||
state = "failregister"; |
||||
throw new RuntimeException("I was told to fail at registration"); |
||||
} |
||||
|
||||
this.context = context; |
||||
context |
||||
.getService(PicoCLIOptions.class) |
||||
.ifPresent( |
||||
picoCLIOptions -> picoCLIOptions.addPicoCLIOptions("Test Plugin", TestPlugin.this)); |
||||
|
||||
writeSignal("registered"); |
||||
state = "registered"; |
||||
} |
||||
|
||||
@Override |
||||
public void start() { |
||||
LOG.info("Starting. Test Option is '{}'", testOption); |
||||
state = "starting"; |
||||
|
||||
if ("FAILSTART".equals(testOption)) { |
||||
state = "failstart"; |
||||
throw new RuntimeException("I was told to fail at startup"); |
||||
} |
||||
|
||||
listenerReference = |
||||
context |
||||
.getService(PantheonEvents.class) |
||||
.map( |
||||
pantheonEvents -> |
||||
pantheonEvents.addBlockAddedListener( |
||||
s -> System.out.println("BlockAdded - " + s))); |
||||
|
||||
writeSignal("started"); |
||||
state = "started"; |
||||
} |
||||
|
||||
@Override |
||||
public void stop() { |
||||
LOG.info("Stopping. Test Option is '{}'", testOption); |
||||
state = "stopping"; |
||||
|
||||
if ("FAILSTOP".equals(testOption)) { |
||||
state = "failstop"; |
||||
throw new RuntimeException("I was told to fail at stop"); |
||||
} |
||||
|
||||
listenerReference.ifPresent( |
||||
reference -> |
||||
context |
||||
.getService(PantheonEvents.class) |
||||
.ifPresent(pantheonEvents -> pantheonEvents.removeBlockAddedObserver(reference))); |
||||
|
||||
writeSignal("stopped"); |
||||
state = "stopped"; |
||||
} |
||||
|
||||
/** State is used to signal unit tests about the lifecycle */ |
||||
public String getState() { |
||||
return state; |
||||
} |
||||
|
||||
/** This is used to signal to the acceptance test that certain tasks were completed. */ |
||||
private void writeSignal(final String signal) { |
||||
try { |
||||
final File callbackFile = |
||||
new File(System.getProperty("pantheon.plugins.dir", "plugins"), "testPlugin." + signal); |
||||
if (!callbackFile.getParentFile().exists()) { |
||||
callbackFile.getParentFile().mkdirs(); |
||||
callbackFile.getParentFile().deleteOnExit(); |
||||
} |
||||
Files.write( |
||||
callbackFile.toPath(), |
||||
Collections.singletonList( |
||||
signal + "\ntestOption=" + testOption + "\nid=" + System.identityHashCode(this))); |
||||
callbackFile.deleteOnExit(); |
||||
} catch (final IOException ioe) { |
||||
throw new RuntimeException(ioe); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,157 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* 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 tech.pegasys.pantheon.plugins.internal; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
||||
|
||||
import tech.pegasys.pantheon.plugins.PantheonPlugin; |
||||
import tech.pegasys.pantheon.plugins.TestPlugin; |
||||
|
||||
import java.io.File; |
||||
import java.io.IOException; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.Path; |
||||
import java.util.List; |
||||
import java.util.Optional; |
||||
|
||||
import org.assertj.core.api.ThrowableAssert; |
||||
import org.junit.After; |
||||
import org.junit.BeforeClass; |
||||
import org.junit.Test; |
||||
|
||||
public class PantheonPluginContextImplTest { |
||||
|
||||
@BeforeClass |
||||
public static void createFakePluginDir() throws IOException { |
||||
if (System.getProperty("pantheon.plugins.dir") == null) { |
||||
final Path pluginDir = Files.createTempDirectory("pantheonTest"); |
||||
pluginDir.toFile().deleteOnExit(); |
||||
System.setProperty("pantheon.plugins.dir", pluginDir.toAbsolutePath().toString()); |
||||
} |
||||
} |
||||
|
||||
@After |
||||
public void clearTestPluginState() { |
||||
System.clearProperty("testPlugin.testOption"); |
||||
} |
||||
|
||||
@Test |
||||
public void verifyEverythingGoesSmoothly() { |
||||
final PantheonPluginContextImpl contextImpl = new PantheonPluginContextImpl(); |
||||
|
||||
assertThat(contextImpl.getPlugins()).isEmpty(); |
||||
contextImpl.registerPlugins(new File(".").toPath()); |
||||
assertThat(contextImpl.getPlugins()).isNotEmpty(); |
||||
|
||||
final Optional<TestPlugin> testPluginOptional = findTestPlugin(contextImpl.getPlugins()); |
||||
assertThat(testPluginOptional).isPresent(); |
||||
final TestPlugin testPlugin = testPluginOptional.get(); |
||||
assertThat(testPlugin.getState()).isEqualTo("registered"); |
||||
|
||||
contextImpl.startPlugins(); |
||||
assertThat(testPlugin.getState()).isEqualTo("started"); |
||||
|
||||
contextImpl.stopPlugins(); |
||||
assertThat(testPlugin.getState()).isEqualTo("stopped"); |
||||
} |
||||
|
||||
@Test |
||||
public void registrationErrorsHandledSmoothly() { |
||||
final PantheonPluginContextImpl contextImpl = new PantheonPluginContextImpl(); |
||||
System.setProperty("testPlugin.testOption", "FAILREGISTER"); |
||||
|
||||
assertThat(contextImpl.getPlugins()).isEmpty(); |
||||
contextImpl.registerPlugins(new File(".").toPath()); |
||||
assertThat(contextImpl.getPlugins()).isEmpty(); |
||||
|
||||
contextImpl.startPlugins(); |
||||
assertThat(contextImpl.getPlugins()).isEmpty(); |
||||
|
||||
contextImpl.stopPlugins(); |
||||
assertThat(contextImpl.getPlugins()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
public void startErrorsHandledSmoothly() { |
||||
final PantheonPluginContextImpl contextImpl = new PantheonPluginContextImpl(); |
||||
System.setProperty("testPlugin.testOption", "FAILSTART"); |
||||
|
||||
assertThat(contextImpl.getPlugins()).isEmpty(); |
||||
contextImpl.registerPlugins(new File(".").toPath()); |
||||
assertThat(contextImpl.getPlugins()).isNotEmpty(); |
||||
|
||||
final Optional<TestPlugin> testPluginOptional = findTestPlugin(contextImpl.getPlugins()); |
||||
assertThat(testPluginOptional).isPresent(); |
||||
final TestPlugin testPlugin = testPluginOptional.get(); |
||||
assertThat(testPlugin.getState()).isEqualTo("registered"); |
||||
|
||||
contextImpl.startPlugins(); |
||||
assertThat(testPlugin.getState()).isEqualTo("failstart"); |
||||
assertThat(contextImpl.getPlugins()).isEmpty(); |
||||
|
||||
contextImpl.stopPlugins(); |
||||
assertThat(contextImpl.getPlugins()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
public void stopErrorsHandledSmoothly() { |
||||
final PantheonPluginContextImpl contextImpl = new PantheonPluginContextImpl(); |
||||
System.setProperty("testPlugin.testOption", "FAILSTOP"); |
||||
|
||||
assertThat(contextImpl.getPlugins()).isEmpty(); |
||||
contextImpl.registerPlugins(new File(".").toPath()); |
||||
assertThat(contextImpl.getPlugins()).isNotEmpty(); |
||||
|
||||
final Optional<TestPlugin> testPluginOptional = findTestPlugin(contextImpl.getPlugins()); |
||||
assertThat(testPluginOptional).isPresent(); |
||||
final TestPlugin testPlugin = testPluginOptional.get(); |
||||
assertThat(testPlugin.getState()).isEqualTo("registered"); |
||||
|
||||
contextImpl.startPlugins(); |
||||
assertThat(testPlugin.getState()).isEqualTo("started"); |
||||
|
||||
contextImpl.stopPlugins(); |
||||
assertThat(testPlugin.getState()).isEqualTo("failstop"); |
||||
} |
||||
|
||||
@Test |
||||
public void lifecycleExceptions() throws Throwable { |
||||
final PantheonPluginContextImpl contextImpl = new PantheonPluginContextImpl(); |
||||
final ThrowableAssert.ThrowingCallable registerPlugins = |
||||
() -> contextImpl.registerPlugins(new File(".").toPath()); |
||||
|
||||
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::startPlugins); |
||||
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::stopPlugins); |
||||
|
||||
registerPlugins.call(); |
||||
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(registerPlugins); |
||||
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::stopPlugins); |
||||
|
||||
contextImpl.startPlugins(); |
||||
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(registerPlugins); |
||||
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::startPlugins); |
||||
|
||||
contextImpl.stopPlugins(); |
||||
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(registerPlugins); |
||||
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::startPlugins); |
||||
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::stopPlugins); |
||||
} |
||||
|
||||
private Optional<TestPlugin> findTestPlugin(final List<PantheonPlugin> plugins) { |
||||
return plugins.stream() |
||||
.filter(p -> p instanceof TestPlugin) |
||||
.map(p -> (TestPlugin) p) |
||||
.findFirst(); |
||||
} |
||||
} |
Loading…
Reference in new issue