Events Plugin - Add initial "NewBlockPropagated" event message (#1463)

* It returns for any block we would broadcast to other peers, when
  we would broadcast them.
* It returns a JSON String containing hash, number, and timestamp
* This event data is not set in stone, it may change in type or content.
* Acceptance tests and unit tests got a re-work away from the assumption
  that there is only one plugin type.

Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
pull/2/head
Danno Ferrin 6 years ago committed by GitHub
parent a8fb76ba63
commit 8d1e224c96
  1. 61
      acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/plugins/PantheonEventsPluginTest.java
  2. 2
      acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/plugins/PicoCLIOptionsPluginTest.java
  3. 4
      ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/manager/EthProtocolManager.java
  4. 13
      ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/BlockBroadcaster.java
  5. 3
      ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/DefaultSynchronizer.java
  6. 19
      pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java
  7. 8
      pantheon/src/main/java/tech/pegasys/pantheon/controller/PantheonController.java
  8. 6
      pantheon/src/main/java/tech/pegasys/pantheon/controller/PantheonControllerBuilder.java
  9. 53
      pantheon/src/main/java/tech/pegasys/pantheon/services/PantheonEventsImpl.java
  10. 31
      pantheon/src/main/java/tech/pegasys/pantheon/services/PicoCLIOptionsImpl.java
  11. 15
      pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java
  12. 95
      pantheon/src/test/java/tech/pegasys/pantheon/services/PantheonEventsImplTest.java
  13. 80
      pantheon/src/test/java/tech/pegasys/pantheon/services/PicoCLIOptionsImplTest.java
  14. 12
      plugins/src/main/java/tech/pegasys/pantheon/plugins/services/PantheonEvents.java
  15. 77
      plugins/src/test/java/tech/pegasys/pantheon/plugins/TestPantheonEventsPlugin.java
  16. 27
      plugins/src/test/java/tech/pegasys/pantheon/plugins/TestPicoCLIPlugin.java
  17. 58
      plugins/src/test/java/tech/pegasys/pantheon/plugins/internal/PantheonPluginContextImplTest.java

@ -0,0 +1,61 @@
/*
* 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 tech.pegasys.pantheon.tests.acceptance.dsl.AcceptanceTestBase;
import tech.pegasys.pantheon.tests.acceptance.dsl.node.PantheonNode;
import java.io.File;
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.Test;
public class PantheonEventsPluginTest extends AcceptanceTestBase {
private PantheonNode pluginNode;
private PantheonNode minerNode;
@Before
public void setUp() throws Exception {
minerNode = pantheon.createMinerNode("minerNode");
pluginNode =
pantheon.createPluginsNode(
"node1", Collections.singletonList("testPlugin"), Collections.emptyList());
cluster.start(pluginNode, minerNode);
}
@Test
public void blockIsAnnounded() {
waitForFile(pluginNode.homeDirectory().resolve("plugins/newBlock.2"));
}
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;
}
});
}
}

@ -30,7 +30,7 @@ import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
public class PluginsAcceptanceTest extends AcceptanceTestBase {
public class PicoCLIOptionsPluginTest extends AcceptanceTestBase {
private PantheonNode node;
// context: https://en.wikipedia.org/wiki/The_Magic_Words_are_Squeamish_Ossifrage

@ -140,6 +140,10 @@ public class EthProtocolManager implements ProtocolManager, MinedBlockObserver {
return ethContext;
}
public BlockBroadcaster getBlockBroadcaster() {
return blockBroadcaster;
}
@Override
public String getSupportedProtocol() {
return EthProtocol.NAME;

@ -16,8 +16,11 @@ import tech.pegasys.pantheon.ethereum.core.Block;
import tech.pegasys.pantheon.ethereum.eth.manager.EthContext;
import tech.pegasys.pantheon.ethereum.eth.messages.NewBlockMessage;
import tech.pegasys.pantheon.ethereum.p2p.api.PeerConnection;
import tech.pegasys.pantheon.util.Subscribers;
import tech.pegasys.pantheon.util.uint.UInt256;
import java.util.function.Consumer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -25,12 +28,22 @@ public class BlockBroadcaster {
private static final Logger LOG = LogManager.getLogger();
private final EthContext ethContext;
private final Subscribers<Consumer<Block>> blockPropagatedSubscribers = new Subscribers<>();
public BlockBroadcaster(final EthContext ethContext) {
this.ethContext = ethContext;
}
public long subscribePropagateNewBlocks(final Consumer<Block> callback) {
return blockPropagatedSubscribers.subscribe(callback);
}
public void unsubscribePropagateNewBlocks(final long id) {
blockPropagatedSubscribers.unsubscribe(id);
}
public void propagate(final Block block, final UInt256 totalDifficulty) {
blockPropagatedSubscribers.forEach(listener -> listener.accept(block));
final NewBlockMessage newBlockMessage = NewBlockMessage.create(block, totalDifficulty);
ethContext
.getEthPeers()

@ -55,6 +55,7 @@ public class DefaultSynchronizer<C> implements Synchronizer {
final ProtocolSchedule<C> protocolSchedule,
final ProtocolContext<C> protocolContext,
final WorldStateStorage worldStateStorage,
final BlockBroadcaster blockBroadcaster,
final EthContext ethContext,
final SyncState syncState,
final Path dataDirectory,
@ -78,7 +79,7 @@ public class DefaultSynchronizer<C> implements Synchronizer {
syncState,
new PendingBlocks(),
metricsSystem,
new BlockBroadcaster(ethContext));
blockBroadcaster);
this.fullSyncDownloader =
new FullSyncDownloader<>(

@ -66,7 +66,10 @@ import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.metrics.prometheus.PrometheusMetricsSystem;
import tech.pegasys.pantheon.metrics.vertx.VertxMetricsAdapterFactory;
import tech.pegasys.pantheon.plugins.internal.PantheonPluginContextImpl;
import tech.pegasys.pantheon.plugins.services.PantheonEvents;
import tech.pegasys.pantheon.plugins.services.PicoCLIOptions;
import tech.pegasys.pantheon.services.PantheonEventsImpl;
import tech.pegasys.pantheon.services.PicoCLIOptionsImpl;
import tech.pegasys.pantheon.services.kvstore.RocksDbConfiguration;
import tech.pegasys.pantheon.util.BlockImporter;
import tech.pegasys.pantheon.util.InvalidConfigurationException;
@ -644,9 +647,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
"Ethereum Wire Protocol",
ethereumWireConfigurationBuilder));
pantheonPluginContext.addService(
PicoCLIOptions.class,
(namespace, optionObject) -> commandLine.addMixin("Plugin " + namespace, optionObject));
pantheonPluginContext.addService(PicoCLIOptions.class, new PicoCLIOptionsImpl(commandLine));
pantheonPluginContext.registerPlugins(pluginsDir());
// Create a handler that will search for a config file option and use it for
@ -729,10 +730,16 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
ensureAllNodesAreInWhitelist(
staticNodes.stream().map(EnodeURL::toURI).collect(Collectors.toList()), p));
final PantheonController<?> pantheonController = buildController();
final MetricsConfiguration metricsConfiguration = metricsConfiguration();
pantheonPluginContext.addService(
PantheonEvents.class,
new PantheonEventsImpl((pantheonController.getProtocolManager().getBlockBroadcaster())));
pantheonPluginContext.startPlugins();
synchronize(
buildController(),
pantheonController,
p2pEnabled,
peerDiscoveryEnabled,
ethNetworkConfig,
@ -742,7 +749,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
graphQLRpcConfiguration,
jsonRpcConfiguration,
webSocketConfiguration,
metricsConfiguration(),
metricsConfiguration,
permissioningConfiguration,
staticNodes);
} catch (final Exception e) {
@ -1234,7 +1241,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
if (isFullInstantiation()) {
final String pluginsDir = System.getProperty("pantheon.plugins.dir");
if (pluginsDir == null) {
return new File("plugins").toPath();
return new File(System.getProperty("pantheon.home", "."), "plugins").toPath();
} else {
return new File(pluginsDir).toPath();
}

@ -20,6 +20,7 @@ import tech.pegasys.pantheon.ethereum.ProtocolContext;
import tech.pegasys.pantheon.ethereum.blockcreation.MiningCoordinator;
import tech.pegasys.pantheon.ethereum.core.PrivacyParameters;
import tech.pegasys.pantheon.ethereum.core.Synchronizer;
import tech.pegasys.pantheon.ethereum.eth.manager.EthProtocolManager;
import tech.pegasys.pantheon.ethereum.eth.transactions.TransactionPool;
import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApi;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod;
@ -35,6 +36,7 @@ public class PantheonController<C> implements java.io.Closeable {
public static final String DATABASE_PATH = "database";
private final ProtocolSchedule<C> protocolSchedule;
private final ProtocolContext<C> protocolContext;
private final EthProtocolManager ethProtocolManager;
private final GenesisConfigOptions genesisConfigOptions;
private final SubProtocolConfiguration subProtocolConfiguration;
private final KeyPair keyPair;
@ -49,6 +51,7 @@ public class PantheonController<C> implements java.io.Closeable {
PantheonController(
final ProtocolSchedule<C> protocolSchedule,
final ProtocolContext<C> protocolContext,
final EthProtocolManager ethProtocolManager,
final GenesisConfigOptions genesisConfigOptions,
final SubProtocolConfiguration subProtocolConfiguration,
final Synchronizer synchronizer,
@ -60,6 +63,7 @@ public class PantheonController<C> implements java.io.Closeable {
final Runnable close) {
this.protocolSchedule = protocolSchedule;
this.protocolContext = protocolContext;
this.ethProtocolManager = ethProtocolManager;
this.genesisConfigOptions = genesisConfigOptions;
this.subProtocolConfiguration = subProtocolConfiguration;
this.synchronizer = synchronizer;
@ -79,6 +83,10 @@ public class PantheonController<C> implements java.io.Closeable {
return protocolSchedule;
}
public EthProtocolManager getProtocolManager() {
return ethProtocolManager;
}
public GenesisConfigOptions getGenesisConfigOptions() {
return genesisConfigOptions;
}

@ -64,6 +64,7 @@ public abstract class PantheonControllerBuilder<C> {
protected GenesisConfigFile genesisConfig;
protected SynchronizerConfiguration syncConfig;
protected EthProtocolManager ethProtocolManager;
protected EthereumWireProtocolConfiguration ethereumWireProtocolConfiguration;
protected Integer networkId;
protected MiningParameters miningParameters;
@ -196,8 +197,7 @@ public abstract class PantheonControllerBuilder<C> {
final MutableBlockchain blockchain = protocolContext.getBlockchain();
final boolean fastSyncEnabled = syncConfig.syncMode().equals(SyncMode.FAST);
final EthProtocolManager ethProtocolManager =
createEthProtocolManager(protocolContext, fastSyncEnabled);
ethProtocolManager = createEthProtocolManager(protocolContext, fastSyncEnabled);
final SyncState syncState =
new SyncState(blockchain, ethProtocolManager.ethContext().getEthPeers());
final Synchronizer synchronizer =
@ -206,6 +206,7 @@ public abstract class PantheonControllerBuilder<C> {
protocolSchedule,
protocolContext,
protocolContext.getWorldStateArchive().getStorage(),
ethProtocolManager.getBlockBroadcaster(),
ethProtocolManager.ethContext(),
syncState,
dataDirectory,
@ -250,6 +251,7 @@ public abstract class PantheonControllerBuilder<C> {
return new PantheonController<>(
protocolSchedule,
protocolContext,
ethProtocolManager,
genesisConfig.getConfigOptions(),
subProtocolConfiguration,
synchronizer,

@ -0,0 +1,53 @@
/*
* 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.services;
import tech.pegasys.pantheon.ethereum.core.Block;
import tech.pegasys.pantheon.ethereum.eth.sync.BlockBroadcaster;
import tech.pegasys.pantheon.plugins.services.PantheonEvents;
import com.google.common.collect.ImmutableMap;
import io.vertx.core.json.Json;
public class PantheonEventsImpl implements PantheonEvents {
private final BlockBroadcaster blockBroadcaster;
public PantheonEventsImpl(final BlockBroadcaster blockBroadcaster) {
this.blockBroadcaster = blockBroadcaster;
}
@Override
public Object addNewBlockPropagatedListener(final NewBlockPropagatedListener listener) {
return blockBroadcaster.subscribePropagateNewBlocks(
block -> dispatchNewBlockPropagatedMessage(block, listener));
}
@Override
public void removeNewBlockPropagatedListener(final Object listenerIdentifier) {
if (listenerIdentifier instanceof Long) {
blockBroadcaster.unsubscribePropagateNewBlocks((Long) listenerIdentifier);
}
}
private void dispatchNewBlockPropagatedMessage(
final Block block, final NewBlockPropagatedListener listener) {
final ImmutableMap<Object, Object> result =
new ImmutableMap.Builder<>()
.put("type", "NewBlock")
.put("blockHash", block.getHash().toString())
.put("blockNumber", block.getHeader().getNumber())
.put("timestamp", block.getHeader().getTimestamp())
.build();
listener.newBlockPropagated(Json.encode(result));
}
}

@ -0,0 +1,31 @@
/*
* 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.services;
import tech.pegasys.pantheon.plugins.services.PicoCLIOptions;
import picocli.CommandLine;
public class PicoCLIOptionsImpl implements PicoCLIOptions {
private final CommandLine commandLine;
public PicoCLIOptionsImpl(final CommandLine commandLine) {
this.commandLine = commandLine;
}
@Override
public void addPicoCLIOptions(final String namespace, final Object optionObject) {
commandLine.addMixin("Plugin " + namespace, optionObject);
}
}

@ -26,6 +26,8 @@ import tech.pegasys.pantheon.controller.PantheonController;
import tech.pegasys.pantheon.controller.PantheonControllerBuilder;
import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair;
import tech.pegasys.pantheon.ethereum.eth.EthereumWireProtocolConfiguration;
import tech.pegasys.pantheon.ethereum.eth.manager.EthProtocolManager;
import tech.pegasys.pantheon.ethereum.eth.sync.BlockBroadcaster;
import tech.pegasys.pantheon.ethereum.eth.sync.SynchronizerConfiguration;
import tech.pegasys.pantheon.ethereum.graphqlrpc.GraphQLRpcConfiguration;
import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration;
@ -55,7 +57,6 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import picocli.CommandLine;
import picocli.CommandLine.Help.Ansi;
@ -78,6 +79,8 @@ public abstract class CommandTestAbstract {
@Mock PantheonController.Builder mockControllerBuilderFactory;
@Mock PantheonControllerBuilder<Void> mockControllerBuilder;
@Mock EthProtocolManager mockEthProtocolManager;
@Mock BlockBroadcaster mockBlockBroadcaster;
@Mock SynchronizerConfiguration.Builder mockSyncConfBuilder;
@Mock EthereumWireProtocolConfiguration.Builder mockEthereumWireProtocolConfigurationBuilder;
@Mock SynchronizerConfiguration mockSyncConf;
@ -112,9 +115,9 @@ public abstract class CommandTestAbstract {
@Before
public void initMocks() throws Exception {
doReturn(mockControllerBuilder).when(mockControllerBuilderFactory).fromEthNetworkConfig(any());
// doReturn used because of generic PantheonController
Mockito.doReturn(mockController).when(mockControllerBuilder).build();
doReturn(mockControllerBuilder).when(mockControllerBuilderFactory).fromEthNetworkConfig(any());
when(mockControllerBuilder.synchronizerConfiguration(any())).thenReturn(mockControllerBuilder);
when(mockControllerBuilder.ethereumWireProtocolConfiguration(any()))
.thenReturn(mockControllerBuilder);
@ -129,6 +132,12 @@ public abstract class CommandTestAbstract {
when(mockControllerBuilder.privacyParameters(any())).thenReturn(mockControllerBuilder);
when(mockControllerBuilder.clock(any())).thenReturn(mockControllerBuilder);
// doReturn used because of generic PantheonController
doReturn(mockController).when(mockControllerBuilder).build();
when(mockController.getProtocolManager()).thenReturn(mockEthProtocolManager);
when(mockEthProtocolManager.getBlockBroadcaster()).thenReturn(mockBlockBroadcaster);
when(mockSyncConfBuilder.syncMode(any())).thenReturn(mockSyncConfBuilder);
when(mockSyncConfBuilder.maxTrailingPeers(anyInt())).thenReturn(mockSyncConfBuilder);
when(mockSyncConfBuilder.fastSyncMinimumPeerCount(anyInt())).thenReturn(mockSyncConfBuilder);

@ -0,0 +1,95 @@
/*
* 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.services;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import tech.pegasys.pantheon.ethereum.core.Block;
import tech.pegasys.pantheon.ethereum.core.BlockBody;
import tech.pegasys.pantheon.ethereum.core.BlockHeaderTestFixture;
import tech.pegasys.pantheon.ethereum.eth.manager.EthContext;
import tech.pegasys.pantheon.ethereum.eth.manager.EthPeers;
import tech.pegasys.pantheon.ethereum.eth.sync.BlockBroadcaster;
import tech.pegasys.pantheon.util.uint.UInt256;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class PantheonEventsImplTest {
@Mock private EthPeers ethPeers;
@Mock private EthContext mockEthContext;
private BlockBroadcaster blockBroadcaster;
private PantheonEventsImpl serviceImpl;
@Before
public void setUp() {
when(ethPeers.streamAvailablePeers()).thenReturn(Stream.empty()).thenReturn(Stream.empty());
when(mockEthContext.getEthPeers()).thenReturn(ethPeers);
blockBroadcaster = new BlockBroadcaster(mockEthContext);
serviceImpl = new PantheonEventsImpl(blockBroadcaster);
}
@Test
public void eventFiresAfterSubscribe() {
final AtomicReference<String> result = new AtomicReference<>();
serviceImpl.addNewBlockPropagatedListener(result::set);
assertThat(result.get()).isNull();
blockBroadcaster.propagate(generateBlock(), UInt256.of(1));
assertThat(result.get()).isNotEmpty();
}
@Test
public void eventDoesNotFireAfterUnsubscribe() {
final AtomicReference<String> result = new AtomicReference<>();
final Object id = serviceImpl.addNewBlockPropagatedListener(result::set);
assertThat(result.get()).isNull();
blockBroadcaster.propagate(generateBlock(), UInt256.of(1));
serviceImpl.removeNewBlockPropagatedListener(id);
result.set(null);
blockBroadcaster.propagate(generateBlock(), UInt256.of(1));
assertThat(result.get()).isNull();
}
@Test
public void propagationWithoutSubscriptionsCompletes() {
blockBroadcaster.propagate(generateBlock(), UInt256.of(1));
}
@Test
public void uselessUnsubscribesCompletes() {
serviceImpl.removeNewBlockPropagatedListener("doesNotExist");
serviceImpl.removeNewBlockPropagatedListener(5);
serviceImpl.removeNewBlockPropagatedListener(5L);
}
private Block generateBlock() {
final BlockBody body = new BlockBody(Collections.emptyList(), Collections.emptyList());
return new Block(new BlockHeaderTestFixture().buildHeader(), body);
}
}

@ -0,0 +1,80 @@
/*
* 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.services;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import org.junit.Before;
import org.junit.Test;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.UnmatchedArgumentException;
public class PicoCLIOptionsImplTest {
@Command
static final class SimpleCommand {
@Option(names = "--existing")
String existingOption = "defaultexisting";
}
static final class MixinOptions {
@Option(names = "--mixin")
String mixinOption = "defaultmixin";
}
private SimpleCommand command;
private MixinOptions mixin;
private CommandLine commandLine;
private PicoCLIOptionsImpl serviceImpl;
@Before
public void setUp() throws Exception {
command = new SimpleCommand();
mixin = new MixinOptions();
commandLine = new CommandLine(command);
serviceImpl = new PicoCLIOptionsImpl(commandLine);
serviceImpl.addPicoCLIOptions("Test 1", mixin);
}
@Test
public void testSimpleOptionParse() {
commandLine.parseArgs("--existing", "1", "--mixin", "2");
assertThat(command.existingOption).isEqualTo("1");
assertThat(mixin.mixinOption).isEqualTo("2");
}
@Test
public void testUnsetOptionLeavesDefault() {
commandLine.parseArgs("--existing", "1");
assertThat(command.existingOption).isEqualTo("1");
assertThat(mixin.mixinOption).isEqualTo("defaultmixin");
}
@Test
public void testMixinOptionOnly() {
commandLine.parseArgs("--mixin", "2");
assertThat(command.existingOption).isEqualTo("defaultexisting");
assertThat(mixin.mixinOption).isEqualTo("2");
}
@Test
public void testNotExistantOptionsFail() {
assertThatExceptionOfType(UnmatchedArgumentException.class)
.isThrownBy(() -> commandLine.parseArgs("--does-not-exist", "1"));
}
}

@ -12,23 +12,25 @@
*/
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
* Returns the raw RLP of a block that Pantheon has received 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);
Object addNewBlockPropagatedListener(NewBlockPropagatedListener blockJSONListener);
/**
* Remove the blockAdded listener from pantheon notifications.
*
* @param listenerIdentifier The instance that was returned from addBlockAddedListener;
*/
void removeBlockAddedObserver(Object listenerIdentifier);
void removeNewBlockPropagatedListener(Object listenerIdentifier);
interface NewBlockPropagatedListener {
void newBlockPropagated(String jsonBlock);
}
}

@ -0,0 +1,77 @@
/*
* 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 java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Collections;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import com.google.auto.service.AutoService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@AutoService(PantheonPlugin.class)
public class TestPantheonEventsPlugin implements PantheonPlugin {
private static final Logger LOG = LogManager.getLogger();
private Optional<Object> subscriptionId;
private PantheonContext context;
private final AtomicInteger blockCounter = new AtomicInteger();
@Override
public void register(final PantheonContext context) {
this.context = context;
LOG.info("Regisgered");
}
@Override
public void start() {
subscriptionId =
context
.getService(PantheonEvents.class)
.map(events -> events.addNewBlockPropagatedListener(this::onBlockAnnounce));
LOG.info("Listening with ID#" + subscriptionId);
}
@Override
public void stop() {
subscriptionId.ifPresent(
id ->
context
.getService(PantheonEvents.class)
.ifPresent(pantheonEvents -> pantheonEvents.removeNewBlockPropagatedListener(id)));
LOG.info("No longer listening with ID#" + subscriptionId);
}
private void onBlockAnnounce(final String json) {
final int blockCount = blockCounter.incrementAndGet();
LOG.info("I got a new block! (I've seen {}) - {}", blockCount, json);
try {
final File callbackFile =
new File(System.getProperty("pantheon.plugins.dir", "plugins"), "newBlock." + blockCount);
if (!callbackFile.getParentFile().exists()) {
callbackFile.getParentFile().mkdirs();
callbackFile.getParentFile().deleteOnExit();
}
Files.write(callbackFile.toPath(), Collections.singletonList(json));
callbackFile.deleteOnExit();
} catch (final IOException ioe) {
throw new RuntimeException(ioe);
}
}
}

@ -12,14 +12,12 @@
*/
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;
@ -27,14 +25,11 @@ import org.apache.logging.log4j.Logger;
import picocli.CommandLine.Option;
@AutoService(PantheonPlugin.class)
public class TestPlugin implements PantheonPlugin {
public class TestPicoCLIPlugin 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");
String testOption = System.getProperty("testPicoCLIPlugin.testOption");
private String state = "uninited";
@ -48,11 +43,11 @@ public class TestPlugin implements PantheonPlugin {
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));
picoCLIOptions ->
picoCLIOptions.addPicoCLIOptions("Test PicoCLI Plugin", TestPicoCLIPlugin.this));
writeSignal("registered");
state = "registered";
@ -68,14 +63,6 @@ public class TestPlugin implements PantheonPlugin {
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";
}
@ -90,12 +77,6 @@ public class TestPlugin implements PantheonPlugin {
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";
}

@ -16,7 +16,7 @@ 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 tech.pegasys.pantheon.plugins.TestPicoCLIPlugin;
import java.io.File;
import java.io.IOException;
@ -43,7 +43,7 @@ public class PantheonPluginContextImplTest {
@After
public void clearTestPluginState() {
System.clearProperty("testPlugin.testOption");
System.clearProperty("testPicoCLIPlugin.testOption");
}
@Test
@ -54,75 +54,75 @@ public class PantheonPluginContextImplTest {
contextImpl.registerPlugins(new File(".").toPath());
assertThat(contextImpl.getPlugins()).isNotEmpty();
final Optional<TestPlugin> testPluginOptional = findTestPlugin(contextImpl.getPlugins());
final Optional<TestPicoCLIPlugin> testPluginOptional = findTestPlugin(contextImpl.getPlugins());
assertThat(testPluginOptional).isPresent();
final TestPlugin testPlugin = testPluginOptional.get();
assertThat(testPlugin.getState()).isEqualTo("registered");
final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered");
contextImpl.startPlugins();
assertThat(testPlugin.getState()).isEqualTo("started");
assertThat(testPicoCLIPlugin.getState()).isEqualTo("started");
contextImpl.stopPlugins();
assertThat(testPlugin.getState()).isEqualTo("stopped");
assertThat(testPicoCLIPlugin.getState()).isEqualTo("stopped");
}
@Test
public void registrationErrorsHandledSmoothly() {
final PantheonPluginContextImpl contextImpl = new PantheonPluginContextImpl();
System.setProperty("testPlugin.testOption", "FAILREGISTER");
System.setProperty("testPicoCLIPlugin.testOption", "FAILREGISTER");
assertThat(contextImpl.getPlugins()).isEmpty();
contextImpl.registerPlugins(new File(".").toPath());
assertThat(contextImpl.getPlugins()).isEmpty();
assertThat(contextImpl.getPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
contextImpl.startPlugins();
assertThat(contextImpl.getPlugins()).isEmpty();
assertThat(contextImpl.getPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
contextImpl.stopPlugins();
assertThat(contextImpl.getPlugins()).isEmpty();
assertThat(contextImpl.getPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
}
@Test
public void startErrorsHandledSmoothly() {
final PantheonPluginContextImpl contextImpl = new PantheonPluginContextImpl();
System.setProperty("testPlugin.testOption", "FAILSTART");
System.setProperty("testPicoCLIPlugin.testOption", "FAILSTART");
assertThat(contextImpl.getPlugins()).isEmpty();
contextImpl.registerPlugins(new File(".").toPath());
assertThat(contextImpl.getPlugins()).isNotEmpty();
assertThat(contextImpl.getPlugins()).extracting("class").contains(TestPicoCLIPlugin.class);
final Optional<TestPlugin> testPluginOptional = findTestPlugin(contextImpl.getPlugins());
final Optional<TestPicoCLIPlugin> testPluginOptional = findTestPlugin(contextImpl.getPlugins());
assertThat(testPluginOptional).isPresent();
final TestPlugin testPlugin = testPluginOptional.get();
assertThat(testPlugin.getState()).isEqualTo("registered");
final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered");
contextImpl.startPlugins();
assertThat(testPlugin.getState()).isEqualTo("failstart");
assertThat(contextImpl.getPlugins()).isEmpty();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("failstart");
assertThat(contextImpl.getPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
contextImpl.stopPlugins();
assertThat(contextImpl.getPlugins()).isEmpty();
assertThat(contextImpl.getPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
}
@Test
public void stopErrorsHandledSmoothly() {
final PantheonPluginContextImpl contextImpl = new PantheonPluginContextImpl();
System.setProperty("testPlugin.testOption", "FAILSTOP");
System.setProperty("testPicoCLIPlugin.testOption", "FAILSTOP");
assertThat(contextImpl.getPlugins()).isEmpty();
contextImpl.registerPlugins(new File(".").toPath());
assertThat(contextImpl.getPlugins()).isNotEmpty();
assertThat(contextImpl.getPlugins()).extracting("class").contains(TestPicoCLIPlugin.class);
final Optional<TestPlugin> testPluginOptional = findTestPlugin(contextImpl.getPlugins());
final Optional<TestPicoCLIPlugin> testPluginOptional = findTestPlugin(contextImpl.getPlugins());
assertThat(testPluginOptional).isPresent();
final TestPlugin testPlugin = testPluginOptional.get();
assertThat(testPlugin.getState()).isEqualTo("registered");
final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered");
contextImpl.startPlugins();
assertThat(testPlugin.getState()).isEqualTo("started");
assertThat(testPicoCLIPlugin.getState()).isEqualTo("started");
contextImpl.stopPlugins();
assertThat(testPlugin.getState()).isEqualTo("failstop");
assertThat(testPicoCLIPlugin.getState()).isEqualTo("failstop");
}
@Test
@ -148,10 +148,10 @@ public class PantheonPluginContextImplTest {
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::stopPlugins);
}
private Optional<TestPlugin> findTestPlugin(final List<PantheonPlugin> plugins) {
private Optional<TestPicoCLIPlugin> findTestPlugin(final List<PantheonPlugin> plugins) {
return plugins.stream()
.filter(p -> p instanceof TestPlugin)
.map(p -> (TestPlugin) p)
.filter(p -> p instanceof TestPicoCLIPlugin)
.map(p -> (TestPicoCLIPlugin) p)
.findFirst();
}
}

Loading…
Cancel
Save