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 moduleClass) { + private static UserServiceAdapter createUserServiceAdapter( + Class moduleClass, Injector frameworkInjector) { try { Supplier moduleSupplier = new ReflectiveModuleSupplier(moduleClass); ServiceModule serviceModule = moduleSupplier.get(); @@ -99,25 +141,15 @@ private UserServiceAdapter createUserServiceAdapter(Class 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 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 serviceModule) { @SafeVarargs public final Builder withServices(Class serviceModule, Class... 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); + } + +}