Skip to content

Conversation

MakarovS
Copy link
Contributor

@MakarovS MakarovS commented Apr 11, 2019

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

  • There are no TODOs left in the code
  • Change is covered by automated tests
  • The coding guidelines are followed
  • Public API has Javadoc
  • Method preconditions are checked and documented in the Javadoc of the method
  • Changelog is updated if needed (in case of notable or breaking changes)
  • The continuous integration build passes

Copy link
Contributor

@vitvakatu vitvakatu left a 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.

* Returns the emulated TestKit node context.
*/
public EmulatedNode getEmulatedNode() {
return nativeGetEmulatedNode(nativeHandle.get());
Copy link
Contributor

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);
Copy link
Contributor

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) {
Copy link
Contributor

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

* @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);
Copy link
Contributor

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>)

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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() };
Copy link
Contributor

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());
Copy link
Contributor

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())

@dmitry-timofeev
Copy link
Contributor

Returns null on error

@vitvakatu,

  1. Do these errors have any context (description)? If so, it would be appropriate to include it in the error message of an exception instead of returning null.
  2. Under which circumstances can these errors occur?
  3. Are these errors the client’s fault? If not, who is to blame in case of these errors?

@dmitry-timofeev
Copy link
Contributor

Simply putting checkNotNull(ret) all over the place does not seem like a good idea, neither does throw new UnknownError("Have no idea what happened, but here you go")

@coveralls
Copy link

coveralls commented Apr 11, 2019

Coverage Status

Coverage increased (+0.4%) to 85.794% when pulling 41a2d38 on ECR-3050 into 5c0e2b3 on master.

@MakarovS
Copy link
Contributor Author

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?

@vitvakatu
Copy link
Contributor

@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
Copy link
Contributor

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

Copy link
Contributor

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()))
Copy link
Contributor

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

Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is @since 11

Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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);
Copy link
Contributor

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

Copy link
Contributor

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 =
Copy link
Contributor

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))
Copy link
Contributor

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.
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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?

Copy link
Contributor Author

@MakarovS MakarovS Apr 22, 2019

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?

Copy link
Contributor

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?

* 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>
Copy link
Contributor

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);
Copy link
Contributor

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();
Copy link
Contributor

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.

Copy link
Contributor Author

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();
Copy link
Contributor

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.

Copy link
Contributor Author

@MakarovS MakarovS Apr 17, 2019

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);
Copy link
Contributor

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);
Copy link
Contributor

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;
Copy link
Contributor

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();
Copy link
Contributor

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;
Copy link
Contributor

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;
Copy link
Contributor

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.

Copy link
Contributor

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
Copy link
Contributor

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?].

Copy link
Contributor Author

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?

Copy link
Contributor

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.

RawTransaction rawTransaction = toRawTransaction(transactionMessage);
service.convertToTransaction(rawTransaction);
} catch (Throwable conversionError) {
String message = String.format("Transaction message (%s) is invalid", transactionMessage);
Copy link
Contributor

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());
Copy link
Contributor

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());
Copy link
Contributor

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).

Suggested change
return Sets.newHashSet(setIndex.iterator());
return Sets.newLinkedHashSet(setIndex.iterator());

Copy link
Contributor

@dmitry-timofeev dmitry-timofeev Apr 25, 2019

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));
Copy link
Contributor

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)

@dmitry-timofeev dmitry-timofeev added the Next to merge 🛬 The next PR to merge (that is up-to-date with master) label Apr 25, 2019
@dmitry-timofeev dmitry-timofeev merged commit a0f37a0 into master Apr 25, 2019
@MakarovS MakarovS deleted the ECR-3050 branch April 27, 2019 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Next to merge 🛬 The next PR to merge (that is up-to-date with master)

Development

Successfully merging this pull request may close these issues.

5 participants