-
Notifications
You must be signed in to change notification settings - Fork 30
Add TestKit P2 methods [ECR-3050] #833
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every native method can return null in case of internal error. I think such errors should be handled in Java, but we can do the other way if you wish.
exonum-java-binding/testkit/src/main/java/com/exonum/binding/testkit/TestKit.java
Show resolved
Hide resolved
exonum-java-binding/testkit/src/main/java/com/exonum/binding/testkit/TestKit.java
Show resolved
Hide resolved
* Returns the emulated TestKit node context. | ||
*/ | ||
public EmulatedNode getEmulatedNode() { | ||
return nativeGetEmulatedNode(nativeHandle.get()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
returns null on error
*/ | ||
public Block createBlockWithTransaction(TransactionMessage transaction) { | ||
byte[][] transactionMessageArray = { transaction.toBytes() }; | ||
byte[] block = nativeCreateBlockWithTransactions(nativeHandle.get(), transactionMessageArray); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.... and this one
return new TestKit(nativeHandle, serviceAdapters); | ||
} | ||
|
||
private TestKit(long nativeHandle, List<UserServiceAdapter> serviceAdapters) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
constructor should be before methods
exonum-java-binding/testkit/src/main/java/com/exonum/binding/testkit/TestKit.java
Outdated
Show resolved
Hide resolved
* @see <a href="https://exonum.com/doc/version/0.10/advanced/consensus/specification/#pool-of-unconfirmed-transactions">Pool of Unconfirmed Transactions</a> | ||
*/ | ||
public Block createBlockWithTransactions(Iterable<TransactionMessage> transactions) { | ||
List<TransactionMessage> messageList = Lists.newArrayList(transactions); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this intermediate list can be replaced by com.google.common.collect.Streams#stream(java.lang.Iterable<T>)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Streams
are marked as @Beta
, is it okay for us?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, for a library no.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the reason we repackaged HashCode and friends.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @see <a href="https://exonum.com/doc/version/0.10/advanced/consensus/specification/#pool-of-unconfirmed-transactions">Pool of Unconfirmed Transactions</a> | ||
*/ | ||
public Block createBlockWithTransaction(TransactionMessage transaction) { | ||
byte[][] transactionMessageArray = { transaction.toBytes() }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please reuse the code
return withSnapshot((view) -> { | ||
Blockchain blockchain = Blockchain.newInstance(view); | ||
MapIndex<HashCode, TransactionMessage> txMessages = blockchain.getTxMessages(); | ||
List<TransactionMessage> messageList = ImmutableList.copyOf(txMessages.values()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, can be replaced by Stream.stream(txMessages.values())
|
Simply putting |
About native methods returning null - @vitvakatu correct me if I'm wrong, but you said that it can only happen on panic in core, in which case we will get a RuntimeException rather than null? |
@MakarovS yep |
* | ||
* @param validatorId validator id of the validator node, less or equal to 0 in case of an | ||
* auditor node | ||
* @param validatorId validator id of the validator node, less than 0 in case of an auditor node |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about:
identifier of the validator node; or any negative value if the node is an auditor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, take a look at #841. -1
is used as the special value for auditor node
List<TransactionMessage> messages = ImmutableList.copyOf(txMessages.values()); | ||
return messages.stream() | ||
.filter(predicate) | ||
.filter(tx -> !txResults.containsKey(tx.hash())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JFYI: recently found sugar methods in Predicate
class such as java.util.function.Predicate#not
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you searched our codebase for possible simplifications?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is @since 11
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... not the Guava Predicates#not
though
checkCorrectServiceNumber(services.size()); | ||
return new TestKit(services, nodeType, validatorCount, timeProvider); | ||
if (nodeType == EmulatedNodeType.VALIDATOR) { | ||
validatorCount += 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we have a test for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this code — please clarify.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't think it is OK as this comment does not explain why this complication is needed. Why don't we just set the total number of validators, not additional, and assume "1" by default?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'll change that. I thought it was easier for users to deal with additional
validators and distinguish these additional
ones as not really emulated. We would have to deal with a little more complicated internal code, but maybe it would make things a bit more straightforward for users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, but the current code does not say that anywhere but in the docs, therefore, gets confusing. Are you saying that people need additional validators quite rarely as it changes almost nothing in testkit behaviour? If this distinction is important, we can leave it as is, but make it clear in names of identifiers.
return toMap(proofMapIndexProxy); | ||
}); | ||
assertThat(map).hasSize(1); | ||
String initialValue = map.get(TestService.INITIAL_ENTRY_KEY); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
org.assertj.core.api.AbstractMapAssert#containsEntry
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or
Map<HashCode, String> expected = ImmutableMap.of(.., ...);
assertThat(map).isEqualTo(expected);
static final short MAX_SERVICE_NUMBER = 256; | ||
private final Injector frameworkInjector = Guice.createInjector(new TestKitFrameworkModule()); | ||
private static final Serializer<Block> BLOCK_SERIALIZER = BlockSerializer.INSTANCE; | ||
private static final Injector frameworkInjector = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it static (same for all Testkits)?
return serviceModules.stream() | ||
.map(this::createUserServiceAdapter) | ||
.map(TestKit::createUserServiceAdapter) | ||
.peek(s -> checkForDuplicateService(s, serviceIds)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest to check a list separately in an independent operation, because such use of peek is discouraged (see the Javadoc) — it produces a side-effect elsewhere and relies on it.
We can do it alternatively like that
List<UserServiceAdapter> services = serviceModules.stream()
.map(TestKit::createUserServiceAdapter)
.collect(toList());
return Maps.uniqueIndex(services, UserServiceAdapter::getId);
this operation will produce an error message including as many duplicates as are in the list: "Multiple entries with same key: 3=bar and 3=foo". It however, won't be in terms of services but in terms of map entries, so our own method might be better if we consider such scenario as realistic.
|
||
private native long nativeCreateTestKit(UserServiceAdapter[] services, boolean auditor, | ||
/** | ||
* Creates a block with the given transaction. In-pool transactions will be ignored. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will they be ignored? Why? It seems deficient: can't I commit a block that includes both in-pool and some incoming transactions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently no, but we could combine some core TestKit methods and add such functionality. Should I add such a method (that commits both input and in-pool transactions) or change this into one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the implications (in terms of testkit usage to test services) of either way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd add both ways, it seems useful to be able to create a block that consists of both in-pool and incoming transactions. We could use core add_tx
method for that - add incoming transactions to the pool and then commit block with all in-pool transactions, but I remember from ECR-2972 there is some problem with add_tx
in 0.11
- @vitvakatu is that still so? Does that prevent us from using this method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure that answers the question. Why would anyone writing a test ignore the in-pool transactions?
exonum-java-binding/testkit/src/main/java/com/exonum/binding/testkit/TestKit.java
Show resolved
Hide resolved
* Creates a block with the given transaction. In-pool transactions will be ignored. | ||
* | ||
* @return created block | ||
* @see <a href="https://exonum.com/doc/version/0.10/advanced/consensus/specification/#pool-of-unconfirmed-transactions">Pool of Unconfirmed Transactions</a> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding references, it is OK not to include in each operation, but it is required to document why this pool is important in operations of this class (at class level).
return toMap(proofMapIndexProxy); | ||
}); | ||
assertThat(map).hasSize(1); | ||
String initialValue = map.get(TestService.INITIAL_ENTRY_KEY); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or
Map<HashCode, String> expected = ImmutableMap.of(.., ...);
assertThat(map).isEqualTo(expected);
private static final TransactionConverter THROWING_TX_CONVERTER = (tx) -> { | ||
throw new IllegalStateException("No transactions in this service: " + tx); | ||
}; | ||
Block block = testKit.createBlock(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is unclear why this is invoked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to create a block so that afterCommit
is invoked and we can test it (by verifying that it indeed submits a transaction). Added a comment
|
||
static short SERVICE_ID = 46; | ||
static String SERVICE_NAME = "Test service"; | ||
testKit.createBlock(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is unclear why this is invoked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to create a block so that afterCommit
submits a transaction into the pool and when we create the block with specified transactions, it ignores this in-pool transaction - then we check that it is indeed still in the pool (together with the second afterCommit
transaction). Added a comment
} | ||
@Test | ||
void createBlockWithTransaction() { | ||
TestKit testKit = TestKit.forService(TestServiceModule.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do all these tests leak the native object?
|
||
@Test | ||
void createBlockWithTransactionsVarargs() { | ||
TestKit testKit = TestKit.forService(TestServiceModule.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code below seems to be the same as above except the function. I'd consider de-duplicating.
checkCorrectServiceNumber(services.size()); | ||
return new TestKit(services, nodeType, validatorCount, timeProvider); | ||
if (nodeType == EmulatedNodeType.VALIDATOR) { | ||
validatorCount += 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't think it is OK as this comment does not explain why this complication is needed. Why don't we just set the total number of validators, not additional, and assume "1" by default?
testKit.withSnapshot((view) -> { | ||
// Check that initialization changed database state | ||
TestSchema testSchema = service.createDataSchema(view); | ||
ProofMapIndexProxy<HashCode, String> proofMapIndexProxy = testSchema.testMap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
testProofMap for this and testMap for j.u.Map
?
bind(TransactionConverter.class).toInstance(THROWING_TX_CONVERTER); | ||
@Test | ||
void afterCommitSubmitsTransaction() { | ||
Block nextBlock; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Here and elsewhere) Why don't we declare these variables inside try-catch and initialize immediately where they are needed?
|
||
@Test | ||
void createBlockWithTransactionWithWrongServiceId() { | ||
Class<RuntimeException> exceptionType = RuntimeException.class; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(1) The thrown exception type is on the wrong abstraction level, RuntimeException communicates nothing. Since it is a testkit,
I would expect either IllegalArgumentException or https://ota4j-team.github.io/opentest4j/docs/current/api/org/opentest4j/AssertionFailedError.html
(2) As we established in the requirements, the error messages must be user-friendly and tell exactly what
the problem is (or which problems are possible). In this case I think there are two possible problems:
- A test developer passed an incorrectly serialized transaction (they intended to pass a proper one).
The implementation of the service is fine. - A test dev passed a correctly serialized transaction, but the implementation of the service
(particularly, TransactionConverter) does not handle that transaction correctly.
In both cases, the actual thrown exception and the details about the transaction message are quite useful
to understand what is broken.
Speaking of the implementation, it might be more convenient to check that each tx message is correct and create
a proper exception in Java, before passing them to the native code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There might be a third case which we do not explicitly support — intentionally passing an invalid transaction. I think we shall either recommend testing that separately from the testkit (just take your TxConverter); or add first-class support (checkCannotDeserialize?). I think the first option is fine for now.
* (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, provide access to its |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this pool thing seems out of place, I'd move it in a separate sentence.
"
... only one node will create ..., execute their operations (...), and provide access to its state [here I mean Snapshots of any state].
Only the emulated node [but what in case it is an auditor?] has a pool of unconfirmed transactions where a service can submit new transaction messages through Node#submitTransaction; or the test code through #commitBlock. All transactions from the pool are committed when a new block is created [is that a reasonable default?].
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What difference does an auditor node make? I don't see how it matters here, as we don't deal with consensus.
or the test code through #commitBlock
Could you expand on this - what commitBlock
do you mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know, ask because it is unclear if an auditor needs to have a pool, because it does not participate in consensus and can process only already committed transaction.
Any commitBlock in testkit that accepts transactions.
…ore sending to native
RawTransaction rawTransaction = toRawTransaction(transactionMessage); | ||
service.convertToTransaction(rawTransaction); | ||
} catch (Throwable conversionError) { | ||
String message = String.format("Transaction message (%s) is invalid", transactionMessage); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I specifically requested this error message to be actionable, and described the possible cases. Please fix the error message to reflect them.
// As only executed transactions are stored in TxResults, it wouldn't contain in-pool | ||
// transactions | ||
ProofMapIndexProxy<HashCode, TransactionResult> txResults = blockchain.getTxResults(); | ||
List<TransactionMessage> messages = ImmutableList.copyOf(txMessages.values()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this code still iterates over all messages?
} | ||
|
||
private <T> Set<T> toSet(KeySetIndexProxy<T> setIndex) { | ||
return Sets.newHashSet(setIndex.iterator()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest using LinkedHashSet to preserve iteration order (= the order in which txs will be committed).
return Sets.newHashSet(setIndex.iterator()); | |
return Sets.newLinkedHashSet(setIndex.iterator()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or, better — just stream them directly, with no intermediate copy — I'll push that
RawTransaction rawTransaction = TestKit.toRawTransaction(message); | ||
String expectedMessage = String.format("Unknown service id (%s) in transaction (%s)", | ||
wrongServiceId, rawTransaction); | ||
assertTrue(thrownException.getMessage().contains(expectedMessage)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and below: assertTrue shall not be used where matchers are available because you won't get expected/actual in case of failure in the output (assertThat ... contains|matches|equals)
Overview
Add TestKit P2 methods, as well as
nativeFreeTestKit
method.Questions:
Should we give a link to documentation in every method that mentions
pool
?Is there a way to avoid making
frameworkInjector
static?See: https://jira.bf.local/browse/ECR-3050
Definition of Done