diff --git a/exonum-java-binding/core/rust/src/testkit/mod.rs b/exonum-java-binding/core/rust/src/testkit/mod.rs
index 56f58b8186..fc3935d117 100644
--- a/exonum-java-binding/core/rust/src/testkit/mod.rs
+++ b/exonum-java-binding/core/rust/src/testkit/mod.rs
@@ -206,6 +206,6 @@ fn create_java_keypair<'a>(
KEYPAIR_CLASS,
"createKeyPair",
KEYPAIR_CTOR_SIGNATURE,
- &[public_key_byte_array.into(), secret_key_byte_array.into()],
+ &[secret_key_byte_array.into(), public_key_byte_array.into()],
)
}
diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/proxy/AbstractCloseableNativeProxy.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/proxy/AbstractCloseableNativeProxy.java
index 2ff906682f..fbb13545ce 100644
--- a/exonum-java-binding/core/src/main/java/com/exonum/binding/proxy/AbstractCloseableNativeProxy.java
+++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/proxy/AbstractCloseableNativeProxy.java
@@ -66,7 +66,6 @@ protected AbstractCloseableNativeProxy(long nativeHandle, boolean dispose) {
this(nativeHandle, dispose, Collections.emptySet());
}
-
/**
* Creates a native proxy.
*
diff --git a/exonum-java-binding/testkit/src/main/java/com/exonum/binding/testkit/EmulatedNode.java b/exonum-java-binding/testkit/src/main/java/com/exonum/binding/testkit/EmulatedNode.java
index feefffd311..58e1eb9881 100644
--- a/exonum-java-binding/testkit/src/main/java/com/exonum/binding/testkit/EmulatedNode.java
+++ b/exonum-java-binding/testkit/src/main/java/com/exonum/binding/testkit/EmulatedNode.java
@@ -22,7 +22,7 @@
import java.util.OptionalInt;
/**
- * Context of the TestKit emulated node.
+ * Context of the TestKit emulated node, i.e., on which it instantiates and executes services.
*/
public class EmulatedNode {
@@ -32,8 +32,8 @@ public class EmulatedNode {
/**
* Creates a context of an emulated node.
*
- * @param validatorId validator id of the validator node, less or equal to 0 in case of an
- * auditor node
+ * @param validatorId identifier of the validator node; or any negative value if the node is an
+ * auditor
* @param serviceKeyPair service key pair of the node
*/
public EmulatedNode(int validatorId, KeyPair serviceKeyPair) {
diff --git a/exonum-java-binding/testkit/src/main/java/com/exonum/binding/testkit/TestKit.java b/exonum-java-binding/testkit/src/main/java/com/exonum/binding/testkit/TestKit.java
index 845f6ed41f..f8abfb038b 100644
--- a/exonum-java-binding/testkit/src/main/java/com/exonum/binding/testkit/TestKit.java
+++ b/exonum-java-binding/testkit/src/main/java/com/exonum/binding/testkit/TestKit.java
@@ -18,32 +18,58 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.collect.Lists.asList;
+import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;
-import com.exonum.binding.proxy.NativeHandle;
+import com.exonum.binding.blockchain.Block;
+import com.exonum.binding.blockchain.Blockchain;
+import com.exonum.binding.blockchain.serialization.BlockSerializer;
+import com.exonum.binding.common.hash.HashCode;
+import com.exonum.binding.common.message.TransactionMessage;
+import com.exonum.binding.common.serialization.Serializer;
+import com.exonum.binding.proxy.AbstractCloseableNativeProxy;
+import com.exonum.binding.proxy.Cleaner;
+import com.exonum.binding.proxy.CloseFailuresException;
import com.exonum.binding.runtime.ReflectiveModuleSupplier;
import com.exonum.binding.service.BlockCommittedEvent;
+import com.exonum.binding.service.Node;
import com.exonum.binding.service.Service;
import com.exonum.binding.service.ServiceModule;
import com.exonum.binding.service.adapters.UserServiceAdapter;
+import com.exonum.binding.storage.database.Snapshot;
+import com.exonum.binding.storage.indices.KeySetIndexProxy;
+import com.exonum.binding.storage.indices.MapIndex;
+import com.exonum.binding.transaction.RawTransaction;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Streams;
import com.google.inject.Guice;
import com.google.inject.Injector;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
import java.util.function.Supplier;
+import java.util.stream.Stream;
import javax.annotation.Nullable;
/**
* TestKit for testing blockchain services. It offers simple network configuration emulation
* (with no real network setup). Although it is possible to add several validator nodes to this
- * network, only one node will create the service instances and will execute their operations
- * (e.g., {@link Service#afterCommit(BlockCommittedEvent)} method logic).
+ * network, only one node will create the service instances, execute their operations (e.g.,
+ * {@linkplain Service#afterCommit(BlockCommittedEvent)} method logic), and provide access to its
+ * state.
+ *
+ *
Only the emulated node has a pool of unconfirmed transactions where a service can submit new
+ * transaction messages through {@linkplain Node#submitTransaction(RawTransaction)}; or the test
+ * code through {@link #createBlockWithTransactions(TransactionMessage...)}. All transactions
+ * from the pool are committed when a new block is created with {@link #createBlock()}.
*
*
When TestKit is created, Exonum blockchain instance is initialized - service instances are
* {@linkplain UserServiceAdapter#initialize(long)} initialized} and genesis block is committed.
@@ -51,42 +77,58 @@
* created.
*
* @see TestKit documentation
+ * Pool of Unconfirmed Transactions
*/
-public final class TestKit {
+public final class TestKit extends AbstractCloseableNativeProxy {
@VisibleForTesting
static final short MAX_SERVICE_NUMBER = 256;
- private final Injector frameworkInjector = Guice.createInjector(new TestKitFrameworkModule());
+ private static final Serializer BLOCK_SERIALIZER = BlockSerializer.INSTANCE;
- private final NativeHandle nativeHandle;
private final Map services = new HashMap<>();
- private TestKit(List> serviceModules, EmulatedNodeType nodeType,
- short validatorCount, @Nullable TimeProvider timeProvider) {
- List serviceAdapters = toUserServiceAdapters(serviceModules);
+ private TestKit(long nativeHandle, Map serviceAdapters) {
+ super(nativeHandle, true);
populateServiceMap(serviceAdapters);
+ }
+
+ private static TestKit newInstance(List> serviceModules,
+ EmulatedNodeType nodeType, short validatorCount,
+ @Nullable TimeProvider timeProvider) {
+ Injector frameworkInjector = Guice.createInjector(new TestKitFrameworkModule());
+ return newInstanceWithInjector(serviceModules, nodeType, validatorCount, timeProvider,
+ frameworkInjector);
+ }
+
+ private static TestKit newInstanceWithInjector(
+ List> serviceModules, EmulatedNodeType nodeType,
+ short validatorCount, @Nullable TimeProvider timeProvider, Injector frameworkInjector) {
+ Map serviceAdapters = toUserServiceAdapters(
+ serviceModules, frameworkInjector);
boolean isAuditorNode = nodeType == EmulatedNodeType.AUDITOR;
- UserServiceAdapter[] userServiceAdapters = serviceAdapters.toArray(new UserServiceAdapter[0]);
- // TODO: fix after native implementation
- nativeHandle = null;
- // nativeHandle = new NativeHandle(
- // nativeCreateTestKit(userServiceAdapters, isAuditorNode, validatorCount, timeProvider));
+ UserServiceAdapter[] userServiceAdapters = serviceAdapters.values()
+ .toArray(new UserServiceAdapter[0]);
+ long nativeHandle = nativeCreateTestKit(userServiceAdapters, isAuditorNode, validatorCount,
+ timeProvider);
+ return new TestKit(nativeHandle, serviceAdapters);
}
/**
* Returns a list of user service adapters created from given service modules.
*/
- private List toUserServiceAdapters(
- List> serviceModules) {
- return serviceModules.stream()
- .map(this::createUserServiceAdapter)
+ private static Map toUserServiceAdapters(
+ List> serviceModules, Injector frameworkInjector) {
+ List services = serviceModules.stream()
+ .map(s -> createUserServiceAdapter(s, frameworkInjector))
.collect(toList());
+ return Maps.uniqueIndex(services, UserServiceAdapter::getId);
}
/**
* Instantiates a service given its module and wraps in a UserServiceAdapter for the native code.
*/
- private UserServiceAdapter createUserServiceAdapter(Class extends ServiceModule> moduleClass) {
+ private static UserServiceAdapter createUserServiceAdapter(
+ Class extends ServiceModule> moduleClass, Injector frameworkInjector) {
try {
Supplier moduleSupplier = new ReflectiveModuleSupplier(moduleClass);
ServiceModule serviceModule = moduleSupplier.get();
@@ -99,25 +141,15 @@ private UserServiceAdapter createUserServiceAdapter(Class extends ServiceModul
}
}
- private void populateServiceMap(List serviceAdapters) {
- for (UserServiceAdapter serviceAdapter: serviceAdapters) {
- checkForDuplicateService(serviceAdapter);
- services.put(serviceAdapter.getId(), serviceAdapter.getService());
- }
- }
-
- private void checkForDuplicateService(UserServiceAdapter newService) {
- short serviceId = newService.getId();
- checkArgument(!services.containsKey(serviceId),
- "Service with id %s was added to the TestKit twice: %s and %s",
- serviceId, services.get(serviceId), newService.getService());
+ private void populateServiceMap(Map serviceAdapters) {
+ serviceAdapters.forEach((id, serviceAdapter) -> services.put(id, serviceAdapter.getService()));
}
/**
* Creates a TestKit network with a single validator node for a single service.
*/
public static TestKit forService(Class extends ServiceModule> serviceModule) {
- return new TestKit(singletonList(serviceModule), EmulatedNodeType.VALIDATOR, (short) 0, null);
+ return newInstance(singletonList(serviceModule), EmulatedNodeType.VALIDATOR, (short) 1, null);
}
/**
@@ -138,7 +170,136 @@ public T getService(short serviceId, Class serviceClass)
return serviceClass.cast(service);
}
- private native long nativeCreateTestKit(UserServiceAdapter[] services, boolean auditor,
+ /**
+ * Creates a block with the given transaction(s). Transactions are applied in the lexicographical
+ * order of their hashes. In-pool transactions will be ignored.
+ *
+ * @return created block
+ * @throws IllegalArgumentException if transactions are malformed or don't belong to this
+ * service
+ */
+ public Block createBlockWithTransactions(TransactionMessage... transactions) {
+ return createBlockWithTransactions(asList(transactions));
+ }
+
+ /**
+ * Creates a block with the given transactions. Transactions are applied in the lexicographical
+ * order of their hashes. In-pool transactions will be ignored.
+ *
+ * @return created block
+ * @throws IllegalArgumentException if transactions are malformed or don't belong to this
+ * service
+ */
+ public Block createBlockWithTransactions(Iterable transactions) {
+ List messageList = ImmutableList.copyOf(transactions);
+ checkTransactions(messageList);
+ byte[][] transactionMessagesArr = messageList.stream()
+ .map(TransactionMessage::toBytes)
+ .toArray(byte[][]::new);
+ byte[] block = nativeCreateBlockWithTransactions(nativeHandle.get(), transactionMessagesArr);
+ return BLOCK_SERIALIZER.fromBytes(block);
+ }
+
+ /**
+ * Creates a block with all in-pool transactions. Transactions are applied in the lexicographical
+ * order of their hashes.
+ *
+ * @return created block
+ */
+ public Block createBlock() {
+ List inPoolTransactions =
+ findTransactionsInPool(transactionMessage -> true);
+ checkTransactions(inPoolTransactions);
+ byte[] block = nativeCreateBlock(nativeHandle.get());
+ return BLOCK_SERIALIZER.fromBytes(block);
+ }
+
+ private void checkTransactions(List transactionMessages) {
+ for (TransactionMessage transactionMessage: transactionMessages) {
+ checkTransaction(transactionMessage);
+ }
+ }
+
+ private void checkTransaction(TransactionMessage transactionMessage) {
+ short serviceId = transactionMessage.getServiceId();
+ RawTransaction rawTransaction = toRawTransaction(transactionMessage);
+ if (!services.containsKey(serviceId)) {
+ String message = String.format("Unknown service id (%s) in transaction (%s)",
+ serviceId, rawTransaction);
+ throw new IllegalArgumentException(message);
+ }
+ Service service = services.get(serviceId);
+ try {
+ service.convertToTransaction(rawTransaction);
+ } catch (Throwable conversionError) {
+ String message = String.format("Service (%s) with id=%s failed to convert transaction (%s)."
+ + " Make sure that the submitted transaction is correctly serialized, and the service's"
+ + " TransactionConverter implementation is correct and handles this transaction as"
+ + " expected.", service.getName(), serviceId, rawTransaction);
+ throw new IllegalArgumentException(message, conversionError);
+ }
+ }
+
+ @VisibleForTesting
+ static RawTransaction toRawTransaction(TransactionMessage transactionMessage) {
+ return RawTransaction.newBuilder()
+ .serviceId(transactionMessage.getServiceId())
+ .transactionId(transactionMessage.getTransactionId())
+ .payload(transactionMessage.getPayload())
+ .build();
+ }
+
+ /**
+ * Returns a list of in-pool transactions that match the given predicate.
+ */
+ public List findTransactionsInPool(Predicate predicate) {
+ return withSnapshot((view) -> {
+ Blockchain blockchain = Blockchain.newInstance(view);
+ MapIndex txMessages = blockchain.getTxMessages();
+ KeySetIndexProxy poolTxsHashes = blockchain.getTransactionPool();
+ return stream(poolTxsHashes)
+ .map(txMessages::get)
+ .collect(toList());
+ });
+ }
+
+ private static Stream stream(KeySetIndexProxy setIndex) {
+ return Streams.stream(setIndex);
+ }
+
+ /**
+ * Performs a given function with a snapshot of the current database state (i.e., the one that
+ * corresponds to the latest committed block). In-pool (not yet processed) transactions are also
+ * accessible with it in {@linkplain Blockchain#getTxMessages() blockchain}.
+ *
+ * @param snapshotFunction a function to execute
+ * @param a type the function returns
+ * @return the result of applying the given function to the database state
+ */
+ public ResultT withSnapshot(Function snapshotFunction) {
+ try (Cleaner cleaner = new Cleaner("TestKit#withSnapshot")) {
+ long snapshotHandle = nativeCreateSnapshot(nativeHandle.get());
+ Snapshot snapshot = Snapshot.newInstance(snapshotHandle, cleaner);
+ return snapshotFunction.apply(snapshot);
+ } catch (CloseFailuresException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns the context of the node that the TestKit emulates (i.e., on which it instantiates and
+ * executes services).
+ */
+ public EmulatedNode getEmulatedNode() {
+ return nativeGetEmulatedNode(nativeHandle.get());
+ }
+
+ @Override
+ protected void disposeInternal() {
+ nativeFreeTestKit(nativeHandle.get());
+ }
+
+ private static native long nativeCreateTestKit(UserServiceAdapter[] services, boolean auditor,
short withValidatorCount, TimeProvider timeProvider);
private native long nativeCreateSnapshot(long nativeHandle);
@@ -149,6 +310,8 @@ private native long nativeCreateTestKit(UserServiceAdapter[] services, boolean a
private native EmulatedNode nativeGetEmulatedNode(long nativeHandle);
+ private native void nativeFreeTestKit(long nativeHandle);
+
/**
* Creates a new builder for the TestKit.
*
@@ -167,24 +330,23 @@ public static Builder builder(EmulatedNodeType nodeType) {
public static final class Builder {
private EmulatedNodeType nodeType;
- private short validatorCount;
+ private short validatorCount = 1;
private List> services = new ArrayList<>();
private TimeProvider timeProvider;
private Builder(EmulatedNodeType nodeType) {
- // TestKit network should have at least one validator node
- if (nodeType == EmulatedNodeType.AUDITOR) {
- validatorCount = 1;
- }
this.nodeType = nodeType;
}
/**
- * Sets number of additional validator nodes in the TestKit network. Note that
+ * Sets number of validator nodes in the TestKit network, should be positive. Note that
* regardless of the configured number of validators, only a single service will be
- * instantiated.
+ * instantiated. Equal to one by default.
+ *
+ * @throws IllegalArgumentException if validatorCount is less than one
*/
public Builder withValidators(short validatorCount) {
+ checkArgument(validatorCount > 0, "TestKit network should have at least one validator node");
this.validatorCount = validatorCount;
return this;
}
@@ -203,7 +365,7 @@ public Builder withService(Class extends ServiceModule> serviceModule) {
@SafeVarargs
public final Builder withServices(Class extends ServiceModule> serviceModule,
Class extends ServiceModule>... serviceModules) {
- return withServices(asList(serviceModule, serviceModules));
+ return withServices(Lists.asList(serviceModule, serviceModules));
}
/**
@@ -228,7 +390,7 @@ public Builder withTimeService(TimeProvider timeProvider) {
*/
public TestKit build() {
checkCorrectServiceNumber(services.size());
- return new TestKit(services, nodeType, validatorCount, timeProvider);
+ return newInstance(services, nodeType, validatorCount, timeProvider);
}
private void checkCorrectServiceNumber(int serviceCount) {
diff --git a/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestKitTest.java b/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestKitTest.java
index c408f01fd6..432f48d654 100644
--- a/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestKitTest.java
+++ b/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestKitTest.java
@@ -16,41 +16,66 @@
package com.exonum.binding.testkit;
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import static com.exonum.binding.testkit.TestService.constructAfterCommitTransaction;
+import static com.exonum.binding.testkit.TestTransaction.BODY_CHARSET;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import com.exonum.binding.blockchain.Block;
+import com.exonum.binding.blockchain.Blockchain;
+import com.exonum.binding.common.blockchain.TransactionResult;
+import com.exonum.binding.common.crypto.CryptoFunction;
+import com.exonum.binding.common.crypto.CryptoFunctions;
+import com.exonum.binding.common.crypto.KeyPair;
+import com.exonum.binding.common.hash.HashCode;
+import com.exonum.binding.common.message.TransactionMessage;
import com.exonum.binding.service.AbstractServiceModule;
import com.exonum.binding.service.Node;
import com.exonum.binding.service.Service;
import com.exonum.binding.service.ServiceModule;
import com.exonum.binding.service.TransactionConverter;
+import com.exonum.binding.storage.database.View;
+import com.exonum.binding.storage.indices.MapIndex;
+import com.exonum.binding.storage.indices.ProofMapIndexProxy;
import com.exonum.binding.transaction.RawTransaction;
import com.exonum.binding.transaction.Transaction;
+import com.exonum.binding.util.LibraryLoader;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
import com.google.inject.Singleton;
import io.vertx.ext.web.Router;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import org.junit.jupiter.api.Test;
class TestKitTest {
+ private static final CryptoFunction CRYPTO_FUNCTION = CryptoFunctions.ed25519();
+ private static final KeyPair KEY_PAIR = CRYPTO_FUNCTION.generateKeyPair();
+
+ static {
+ LibraryLoader.load();
+ }
+
@Test
void createTestKitForSingleService() {
- TestKit testKit = TestKit.forService(TestServiceModule.class);
- Service service = testKit.getService(TestService.SERVICE_ID, TestService.class);
- assertEquals(service.getId(), TestService.SERVICE_ID);
- assertEquals(service.getName(), TestService.SERVICE_NAME);
+ TestService service;
+ try (TestKit testKit = TestKit.forService(TestServiceModule.class)) {
+ service = testKit.getService(TestService.SERVICE_ID, TestService.class);
+ checkTestServiceInitialization(testKit, service);
+ }
}
@Test
void createTestKitWithBuilderForSingleService() {
- TestKit testKit = TestKit.builder(EmulatedNodeType.VALIDATOR)
+ try (TestKit testKit = TestKit.builder(EmulatedNodeType.VALIDATOR)
.withService(TestServiceModule.class)
- .build();
- Service service = testKit.getService(TestService.SERVICE_ID, TestService.class);
- assertEquals(service.getId(), TestService.SERVICE_ID);
- assertEquals(service.getName(), TestService.SERVICE_NAME);
+ .build()) {
+ TestService service = testKit.getService(TestService.SERVICE_ID, TestService.class);
+ checkTestServiceInitialization(testKit, service);
+ }
}
@Test
@@ -65,48 +90,131 @@ void createTestKitWithBuilderForMultipleSameServices() {
@Test
void createTestKitWithBuilderForMultipleDifferentServices() {
- TestKit testKit = TestKit.builder(EmulatedNodeType.VALIDATOR)
+ try (TestKit testKit = TestKit.builder(EmulatedNodeType.VALIDATOR)
.withService(TestServiceModule.class)
.withService(TestServiceModule2.class)
- .build();
- Service service = testKit.getService(TestService.SERVICE_ID, TestService.class);
- Service service2 = testKit.getService(TestService2.SERVICE_ID, TestService2.class);
- assertEquals(service.getId(), TestService.SERVICE_ID);
- assertEquals(service.getName(), TestService.SERVICE_NAME);
- assertEquals(service2.getId(), TestService2.SERVICE_ID);
- assertEquals(service2.getName(), TestService2.SERVICE_NAME);
+ .build()) {
+ TestService service = testKit.getService(TestService.SERVICE_ID, TestService.class);
+ checkTestServiceInitialization(testKit, service);
+ TestService2 service2 = testKit.getService(TestService2.SERVICE_ID, TestService2.class);
+ checkTestService2Initialization(testKit, service2);
+ }
}
@Test
void createTestKitWithBuilderForMultipleDifferentServicesVarargs() {
- TestKit testKit = TestKit.builder(EmulatedNodeType.VALIDATOR)
+ try (TestKit testKit = TestKit.builder(EmulatedNodeType.VALIDATOR)
.withServices(TestServiceModule.class, TestServiceModule2.class)
- .build();
- Service service = testKit.getService(TestService.SERVICE_ID, TestService.class);
- Service service2 = testKit.getService(TestService2.SERVICE_ID, TestService2.class);
- assertEquals(service.getId(), TestService.SERVICE_ID);
- assertEquals(service.getName(), TestService.SERVICE_NAME);
- assertEquals(service2.getId(), TestService2.SERVICE_ID);
- assertEquals(service2.getName(), TestService2.SERVICE_NAME);
+ .build()) {
+ TestService service = testKit.getService(TestService.SERVICE_ID, TestService.class);
+ checkTestServiceInitialization(testKit, service);
+ TestService2 service2 = testKit.getService(TestService2.SERVICE_ID, TestService2.class);
+ checkTestService2Initialization(testKit, service2);
+ }
+ }
+
+ private void checkTestServiceInitialization(TestKit testKit, TestService service) {
+ // Check that TestKit contains an instance of TestService
+ assertThat(service.getId()).isEqualTo(TestService.SERVICE_ID);
+ assertThat(service.getName()).isEqualTo(TestService.SERVICE_NAME);
+
+ // Check that TestService API is mounted
+ Node serviceNode = service.getNode();
+ EmulatedNode emulatedTestKitNode = testKit.getEmulatedNode();
+ assertThat(serviceNode.getPublicKey())
+ .isEqualTo(emulatedTestKitNode.getServiceKeyPair().getPublicKey());
+ testKit.withSnapshot((view) -> {
+ // Check that initialization changed database state
+ TestSchema testSchema = service.createDataSchema(view);
+ ProofMapIndexProxy testProofMap = testSchema.testMap();
+ Map testMap = toMap(testProofMap);
+ Map expected = ImmutableMap.of(
+ TestService.INITIAL_ENTRY_KEY, TestService.INITIAL_ENTRY_VALUE);
+ assertThat(testMap).isEqualTo(expected);
+
+ // Check that genesis block was committed
+ Blockchain blockchain = Blockchain.newInstance(view);
+ assertThat(blockchain.getBlockHashes().size()).isEqualTo(1L);
+ return null;
+ });
+ }
+
+ private void checkTestService2Initialization(TestKit testKit, TestService2 service) {
+ // Check that TestKit contains an instance of TestService2
+ assertThat(service.getId()).isEqualTo(TestService2.SERVICE_ID);
+ assertThat(service.getName()).isEqualTo(TestService2.SERVICE_NAME);
+
+ // Check that TestService2 API is mounted
+ Node serviceNode = service.getNode();
+ EmulatedNode emulatedTestKitNode = testKit.getEmulatedNode();
+ assertThat(serviceNode.getPublicKey())
+ .isEqualTo(emulatedTestKitNode.getServiceKeyPair().getPublicKey());
+ testKit.withSnapshot((view) -> {
+ // Check that genesis block was committed
+ Blockchain blockchain = Blockchain.newInstance(view);
+ assertThat(blockchain.getBlockHashes().size()).isEqualTo(1L);
+ return null;
+ });
+ }
+
+ @Test
+ void createTestKitWithSeveralValidators() {
+ short validatorCount = 2;
+ try (TestKit testKit = TestKit.builder(EmulatedNodeType.VALIDATOR)
+ .withService(TestServiceModule.class)
+ .withValidators(validatorCount)
+ .build()) {
+ testKit.withSnapshot((view) -> {
+ Blockchain blockchain = Blockchain.newInstance(view);
+ assertThat(blockchain.getActualConfiguration().validatorKeys().size())
+ .isEqualTo(validatorCount);
+ return null;
+ });
+ }
+ }
+
+ @Test
+ void createTestKitWithAuditorAndAdditionalValidators() {
+ short validatorCount = 2;
+ try (TestKit testKit = TestKit.builder(EmulatedNodeType.AUDITOR)
+ .withService(TestServiceModule.class)
+ .withValidators(validatorCount)
+ .build()) {
+ testKit.withSnapshot((view) -> {
+ Blockchain blockchain = Blockchain.newInstance(view);
+ assertThat(blockchain.getActualConfiguration().validatorKeys().size())
+ .isEqualTo(validatorCount);
+ return null;
+ });
+ }
+ }
+
+ @Test
+ void setInvalidValidatorCount() {
+ Class exceptionType = IllegalArgumentException.class;
+ short invalidValidatorCount = 0;
+ TestKit.Builder testKitBuilder = TestKit.builder(EmulatedNodeType.VALIDATOR)
+ .withService(TestServiceModule.class);
+ assertThrows(exceptionType, () -> testKitBuilder.withValidators(invalidValidatorCount));
}
@Test
void requestWrongServiceClass() {
Class exceptionType = IllegalArgumentException.class;
- TestKit testKit = TestKit.builder(EmulatedNodeType.VALIDATOR)
+ try (TestKit testKit = TestKit.builder(EmulatedNodeType.VALIDATOR)
.withService(TestServiceModule.class)
- .build();
- assertThrows(exceptionType,
- () -> testKit.getService(TestService.SERVICE_ID, TestService2.class));
+ .build()) {
+ assertThrows(exceptionType,
+ () -> testKit.getService(TestService.SERVICE_ID, TestService2.class));
+ }
}
@Test
void requestWrongServiceId() {
Class exceptionType = IllegalArgumentException.class;
- TestKit testKit = TestKit.builder(EmulatedNodeType.VALIDATOR)
- .withService(TestServiceModule.class)
- .build();
- assertThrows(exceptionType, () -> testKit.getService((short) -1, TestService2.class));
+ try (TestKit testKit = TestKit.forService(TestServiceModule.class)) {
+ assertThrows(exceptionType, () -> testKit.getService((short) -1, TestService2.class));
+ }
}
@Test
@@ -128,55 +236,209 @@ void createTestKitWithoutServices() {
assertThrows(exceptionType, testKitBuilder::build);
}
- public static final class TestServiceModule extends AbstractServiceModule {
+ @Test
+ void createEmptyBlock() {
+ try (TestKit testKit = TestKit.forService(TestServiceModule.class)) {
+ Block block = testKit.createBlock();
+ assertThat(block.getNumTransactions()).isEqualTo(0);
- private static final TransactionConverter THROWING_TX_CONVERTER = (tx) -> {
- throw new IllegalStateException("No transactions in this service: " + tx);
- };
+ testKit.withSnapshot((view) -> {
+ Blockchain blockchain = Blockchain.newInstance(view);
+ assertThat(blockchain.getHeight()).isEqualTo(1);
+ assertThat(block).isEqualTo(blockchain.getBlock(1));
+ return null;
+ });
+ }
+ }
- @Override
- protected void configure() {
- bind(Service.class).to(TestService.class).in(Singleton.class);
- bind(TransactionConverter.class).toInstance(THROWING_TX_CONVERTER);
+ @Test
+ void afterCommitSubmitsTransaction() {
+ try (TestKit testKit = TestKit.forService(TestServiceModule.class)) {
+ // Create a block so that afterCommit transaction is submitted
+ Block block = testKit.createBlock();
+ List inPoolTransactions = testKit
+ .findTransactionsInPool(tx -> tx.getServiceId() == TestService.SERVICE_ID);
+ assertThat(inPoolTransactions).hasSize(1);
+ TransactionMessage inPoolTransaction = inPoolTransactions.get(0);
+ RawTransaction afterCommitTransaction = constructAfterCommitTransaction(block.getHeight());
+
+ assertThat(inPoolTransaction.getServiceId())
+ .isEqualTo(afterCommitTransaction.getServiceId());
+ assertThat(inPoolTransaction.getTransactionId())
+ .isEqualTo(afterCommitTransaction.getTransactionId());
+ assertThat(inPoolTransaction.getPayload()).isEqualTo(afterCommitTransaction.getPayload());
+
+ Block nextBlock = testKit.createBlock();
+ assertThat(nextBlock.getNumTransactions()).isEqualTo(1);
+ assertThat(nextBlock.getHeight()).isEqualTo(2);
}
}
- public static final class TestServiceModule2 extends AbstractServiceModule {
+ @Test
+ void createBlockWithTransactionIgnoresInPoolTransactions() {
+ try (TestKit testKit = TestKit.forService(TestServiceModule.class)) {
+ // Create a block so that afterCommit transaction is submitted
+ testKit.createBlock();
- private static final TransactionConverter THROWING_TX_CONVERTER = (tx) -> {
- throw new IllegalStateException("No transactions in this service: " + tx);
- };
+ Block block = testKit.createBlockWithTransactions();
+ assertThat(block.getNumTransactions()).isEqualTo(0);
- @Override
- protected void configure() {
- bind(Service.class).to(TestService2.class).in(Singleton.class);
- bind(TransactionConverter.class).toInstance(THROWING_TX_CONVERTER);
+ // Two blocks were created, so two afterCommit transactions should be submitted into pool
+ List inPoolTransactions = testKit
+ .findTransactionsInPool(tx -> tx.getServiceId() == TestService.SERVICE_ID);
+ assertThat(inPoolTransactions).hasSize(2);
+ }
+ }
+
+ @Test
+ void createBlockWithSingleTransaction() {
+ try (TestKit testKit = TestKit.forService(TestServiceModule.class)) {
+ TransactionMessage message = constructTestTransactionMessage("Test message");
+ Block block = testKit.createBlockWithTransactions(message);
+ assertThat(block.getNumTransactions()).isEqualTo(1);
+
+ testKit.withSnapshot((view) -> {
+ Blockchain blockchain = Blockchain.newInstance(view);
+ assertThat(blockchain.getHeight()).isEqualTo(1);
+ assertThat(block).isEqualTo(blockchain.getBlock(1));
+ Map transactionResults = toMap(blockchain.getTxResults());
+ assertThat(transactionResults).hasSize(1);
+ TransactionResult transactionResult = transactionResults.get(message.hash());
+ assertThat(transactionResult).isEqualTo(TransactionResult.successful());
+ return null;
+ });
}
}
- static final class TestService implements Service {
+ @Test
+ void createBlockWithTransactions() {
+ try (TestKit testKit = TestKit.forService(TestServiceModule.class)) {
+ TransactionMessage message = constructTestTransactionMessage("Test message");
+ TransactionMessage message2 = constructTestTransactionMessage("Test message 2");
- static short SERVICE_ID = 46;
- static String SERVICE_NAME = "Test service";
+ Block block = testKit.createBlockWithTransactions(ImmutableList.of(message, message2));
+ assertThat(block.getNumTransactions()).isEqualTo(2);
- @Override
- public short getId() {
- return SERVICE_ID;
+ testKit.withSnapshot((view) -> {
+ checkTransactionsCommittedSuccessfully(view, block, message, message2);
+ return null;
+ });
}
+ }
- @Override
- public String getName() {
- return SERVICE_NAME;
+ @Test
+ void createBlockWithTransactionsVarargs() {
+ try (TestKit testKit = TestKit.forService(TestServiceModule.class)) {
+ TransactionMessage message = constructTestTransactionMessage("Test message");
+ TransactionMessage message2 = constructTestTransactionMessage("Test message 2");
+
+ Block block = testKit.createBlockWithTransactions(message, message2);
+ assertThat(block.getNumTransactions()).isEqualTo(2);
+
+ testKit.withSnapshot((view) -> {
+ checkTransactionsCommittedSuccessfully(view, block, message, message2);
+ return null;
+ });
}
+ }
- @Override
- public Transaction convertToTransaction(RawTransaction rawTransaction) {
- throw new UnsupportedOperationException();
+ private TransactionMessage constructTestTransactionMessage(String payload) {
+ return TransactionMessage.builder()
+ .serviceId(TestService.SERVICE_ID)
+ .transactionId(TestTransaction.ID)
+ .payload(payload.getBytes(BODY_CHARSET))
+ .sign(KEY_PAIR, CRYPTO_FUNCTION);
+ }
+
+ private void checkTransactionsCommittedSuccessfully(
+ View view, Block block, TransactionMessage message, TransactionMessage message2) {
+ Blockchain blockchain = Blockchain.newInstance(view);
+ assertThat(blockchain.getHeight()).isEqualTo(1);
+ assertThat(block).isEqualTo(blockchain.getBlock(1));
+ Map transactionResults = toMap(blockchain.getTxResults());
+ assertThat(transactionResults).hasSize(2);
+
+ TransactionResult transactionResult = transactionResults.get(message.hash());
+ assertThat(transactionResult).isEqualTo(TransactionResult.successful());
+ TransactionResult transactionResult2 = transactionResults.get(message2.hash());
+ assertThat(transactionResult2).isEqualTo(TransactionResult.successful());
+ }
+
+ @Test
+ void createBlockWithTransactionWithWrongServiceId() {
+ try (TestKit testKit = TestKit.forService(TestServiceModule.class)) {
+ short wrongServiceId = (short) (TestService.SERVICE_ID + 1);
+ TransactionMessage message = TransactionMessage.builder()
+ .serviceId(wrongServiceId)
+ .transactionId(TestTransaction.ID)
+ .payload("Test message".getBytes(BODY_CHARSET))
+ .sign(KEY_PAIR, CRYPTO_FUNCTION);
+ IllegalArgumentException thrownException = assertThrows(IllegalArgumentException.class,
+ () -> testKit.createBlockWithTransactions(message));
+ RawTransaction rawTransaction = TestKit.toRawTransaction(message);
+ String expectedMessage = String.format("Unknown service id (%s) in transaction (%s)",
+ wrongServiceId, rawTransaction);
+ assertThat(thrownException).hasMessageContaining(expectedMessage);
+ }
+ }
+
+ @Test
+ void createBlockWithTransactionWithWrongTransactionId() {
+ try (TestKit testKit = TestKit.forService(TestServiceModule.class)) {
+ short wrongTransactionId = (short) (TestTransaction.ID + 1);
+ TransactionMessage message = TransactionMessage.builder()
+ .serviceId(TestService.SERVICE_ID)
+ .transactionId(wrongTransactionId)
+ .payload("Test message".getBytes(BODY_CHARSET))
+ .sign(KEY_PAIR, CRYPTO_FUNCTION);
+ IllegalArgumentException thrownException = assertThrows(IllegalArgumentException.class,
+ () -> testKit.createBlockWithTransactions(message));
+ RawTransaction rawTransaction = TestKit.toRawTransaction(message);
+ String expectedMessage = String.format("Service (%s) with id=%s failed to convert"
+ + " transaction (%s). Make sure that the submitted transaction is correctly serialized,"
+ + " and the service's TransactionConverter implementation is correct and handles this"
+ + " transaction as expected.",
+ TestService.SERVICE_NAME, TestService.SERVICE_ID, rawTransaction);
+ assertThat(thrownException).hasMessageContaining(expectedMessage);
+ }
+ }
+
+ @Test
+ void getValidatorEmulatedNode() {
+ try (TestKit testKit = TestKit.forService(TestServiceModule.class)) {
+ EmulatedNode node = testKit.getEmulatedNode();
+ assertThat(node.getNodeType()).isEqualTo(EmulatedNodeType.VALIDATOR);
+ assertThat(node.getValidatorId()).isNotEmpty();
+ assertThat(node.getServiceKeyPair()).isNotNull();
+ }
+ }
+
+ @Test
+ void getAuditorEmulatedNode() {
+ try (TestKit testKit = TestKit.builder(EmulatedNodeType.AUDITOR)
+ .withService(TestServiceModule.class)
+ .build()) {
+ EmulatedNode node = testKit.getEmulatedNode();
+ assertThat(node.getNodeType()).isEqualTo(EmulatedNodeType.AUDITOR);
+ assertThat(node.getValidatorId()).isEmpty();
+ assertThat(node.getServiceKeyPair()).isNotNull();
}
+ }
+
+ private Map toMap(MapIndex mapIndex) {
+ return Maps.toMap(mapIndex.keys(), mapIndex::get);
+ }
+
+ public static final class TestServiceModule2 extends AbstractServiceModule {
+
+ private static final TransactionConverter THROWING_TX_CONVERTER = (tx) -> {
+ throw new IllegalStateException("No transactions in this service: " + tx);
+ };
@Override
- public void createPublicApiHandlers(Node node, Router router) {
- // No-op: no handlers.
+ protected void configure() {
+ bind(Service.class).to(TestService2.class).in(Singleton.class);
+ bind(TransactionConverter.class).toInstance(THROWING_TX_CONVERTER);
}
}
@@ -185,6 +447,8 @@ static final class TestService2 implements Service {
static short SERVICE_ID = 48;
static String SERVICE_NAME = "Test service 2";
+ private Node node;
+
@Override
public short getId() {
return SERVICE_ID;
@@ -195,6 +459,10 @@ public String getName() {
return SERVICE_NAME;
}
+ Node getNode() {
+ return node;
+ }
+
@Override
public Transaction convertToTransaction(RawTransaction rawTransaction) {
throw new UnsupportedOperationException();
@@ -202,7 +470,7 @@ public Transaction convertToTransaction(RawTransaction rawTransaction) {
@Override
public void createPublicApiHandlers(Node node, Router router) {
- // No-op: no handlers.
+ this.node = node;
}
}
}
diff --git a/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestSchema.java b/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestSchema.java
new file mode 100644
index 0000000000..8062667052
--- /dev/null
+++ b/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestSchema.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018 The Exonum Team
+ *
+ * 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 com.exonum.binding.testkit;
+
+import com.exonum.binding.common.hash.HashCode;
+import com.exonum.binding.common.serialization.StandardSerializers;
+import com.exonum.binding.service.Schema;
+import com.exonum.binding.storage.database.View;
+import com.exonum.binding.storage.indices.ProofMapIndexProxy;
+import java.util.Collections;
+import java.util.List;
+
+final class TestSchema implements Schema {
+
+ static final String TEST_MAP_NAME = "TestKitService_map";
+
+ private final View view;
+
+ TestSchema(View view) {
+ this.view = view;
+ }
+
+ ProofMapIndexProxy testMap() {
+ return ProofMapIndexProxy.newInstance(TEST_MAP_NAME, view, StandardSerializers.hash(),
+ StandardSerializers.string());
+ }
+
+ @Override
+ public List getStateHashes() {
+ HashCode rootHash = testMap().getRootHash();
+ return Collections.singletonList(rootHash);
+ }
+}
diff --git a/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestService.java b/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestService.java
new file mode 100644
index 0000000000..a2c03f277f
--- /dev/null
+++ b/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestService.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2019 The Exonum Team
+ *
+ * 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 com.exonum.binding.testkit;
+
+import static com.exonum.binding.testkit.TestTransaction.BODY_CHARSET;
+
+import com.exonum.binding.common.hash.HashCode;
+import com.exonum.binding.common.hash.Hashing;
+import com.exonum.binding.service.AbstractService;
+import com.exonum.binding.service.BlockCommittedEvent;
+import com.exonum.binding.service.InternalServerError;
+import com.exonum.binding.service.Node;
+import com.exonum.binding.storage.database.Fork;
+import com.exonum.binding.storage.database.View;
+import com.exonum.binding.storage.indices.ProofMapIndexProxy;
+import com.exonum.binding.transaction.RawTransaction;
+import io.vertx.ext.web.Router;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+final class TestService extends AbstractService {
+
+ static final HashCode INITIAL_ENTRY_KEY = Hashing.defaultHashFunction()
+ .hashString("initial key", StandardCharsets.UTF_8);
+ static final String INITIAL_ENTRY_VALUE = "initial value";
+ static final String INITIAL_CONFIGURATION = "{ \"version\": \"0.2.0\" }";
+
+ static short SERVICE_ID = 46;
+ static String SERVICE_NAME = "Test service";
+
+ private Node node;
+
+ public TestService() {
+ super(SERVICE_ID, SERVICE_NAME, TestTransaction::from);
+ }
+
+ @Override
+ public short getId() {
+ return SERVICE_ID;
+ }
+
+ @Override
+ public String getName() {
+ return SERVICE_NAME;
+ }
+
+ Node getNode() {
+ return node;
+ }
+
+ @Override
+ protected TestSchema createDataSchema(View view) {
+ return new TestSchema(view);
+ }
+
+ @Override
+ public Optional initialize(Fork fork) {
+ TestSchema schema = createDataSchema(fork);
+ ProofMapIndexProxy testMap = schema.testMap();
+ testMap.put(INITIAL_ENTRY_KEY, INITIAL_ENTRY_VALUE);
+ return Optional.of(INITIAL_CONFIGURATION);
+ }
+
+ @Override
+ public void afterCommit(BlockCommittedEvent event) {
+ long height = event.getHeight();
+ RawTransaction rawTransaction = constructAfterCommitTransaction(height);
+ try {
+ node.submitTransaction(rawTransaction);
+ } catch (InternalServerError e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static RawTransaction constructAfterCommitTransaction(long height) {
+ String payload = "Test message on height " + height;
+ return RawTransaction.newBuilder()
+ .serviceId(SERVICE_ID)
+ .transactionId(TestTransaction.ID)
+ .payload(payload.getBytes(BODY_CHARSET))
+ .build();
+ }
+
+ @Override
+ public void createPublicApiHandlers(Node node, Router router) {
+ this.node = node;
+ }
+}
diff --git a/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestServiceModule.java b/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestServiceModule.java
new file mode 100644
index 0000000000..cb6597148d
--- /dev/null
+++ b/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestServiceModule.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019 The Exonum Team
+ *
+ * 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 com.exonum.binding.testkit;
+
+import com.exonum.binding.service.AbstractServiceModule;
+import com.exonum.binding.service.Service;
+import com.google.inject.Singleton;
+
+public final class TestServiceModule extends AbstractServiceModule {
+
+ @Override
+ protected void configure() {
+ bind(Service.class).to(TestService.class).in(Singleton.class);
+ }
+}
diff --git a/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestTransaction.java b/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestTransaction.java
new file mode 100644
index 0000000000..2599782dff
--- /dev/null
+++ b/exonum-java-binding/testkit/src/test/java/com/exonum/binding/testkit/TestTransaction.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2019 The Exonum Team
+ *
+ * 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 com.exonum.binding.testkit;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.exonum.binding.common.hash.HashCode;
+import com.exonum.binding.common.hash.Hashing;
+import com.exonum.binding.storage.indices.ProofMapIndexProxy;
+import com.exonum.binding.transaction.RawTransaction;
+import com.exonum.binding.transaction.Transaction;
+import com.exonum.binding.transaction.TransactionContext;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+
+public final class TestTransaction implements Transaction {
+
+ static final short ID = 94;
+ static final Charset BODY_CHARSET = StandardCharsets.UTF_8;
+
+ private final String value;
+
+ static TestTransaction from(RawTransaction rawTransaction) {
+ checkArgument(rawTransaction.getServiceId() == TestService.SERVICE_ID);
+ checkArgument(rawTransaction.getTransactionId() == TestTransaction.ID);
+ String value = getValue(rawTransaction);
+ return new TestTransaction(value);
+ }
+
+ private static String getValue(RawTransaction rawTransaction) {
+ try {
+ CharsetDecoder utf8Decoder = createUtf8Decoder();
+ ByteBuffer body = ByteBuffer.wrap(rawTransaction.getPayload());
+ CharBuffer result = utf8Decoder.decode(body);
+ return result.toString();
+ } catch (CharacterCodingException e) {
+ throw new IllegalArgumentException("Cannot decode the message body", e);
+ }
+ }
+
+ private static CharsetDecoder createUtf8Decoder() {
+ return BODY_CHARSET.newDecoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPLACE);
+ }
+
+ private TestTransaction(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public void execute(TransactionContext context) {
+ TestSchema schema = new TestSchema(context.getFork());
+ ProofMapIndexProxy map = schema.testMap();
+ map.put(getKey(), value);
+ }
+
+ private HashCode getKey() {
+ return Hashing.defaultHashFunction()
+ .hashString(value, BODY_CHARSET);
+ }
+
+}