Skip to content

Conversation

MakarovS
Copy link
Contributor

@MakarovS MakarovS commented Apr 2, 2019

Overview

Add TestKit P1 functionality.


See: https://jira.bf.local/browse/ECR-3049

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) - will be updated in a later PR
  • The continuous integration build passes

package com.exonum.binding.testkit;

/**
* Type of the TestKit emulated node.
Copy link
Contributor

Choose a reason for hiding this comment

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

/** 
* Type of the Exonum node emulated by TestKit.
*
* @see <a href="https://exonum.com/doc/version/0.10/glossary/#auditor">Auditor Node</a>
*      <a href="https://exonum.com/doc/version/0.10/glossary/#validator">Validator Node</a>
**/


import java.time.ZonedDateTime;

public class FakeTimeProvider implements TimeProvider {
Copy link
Contributor

Choose a reason for hiding this comment

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

javadoc is missing. Please describe the purpose of this class.

Copy link
Contributor

Choose a reason for hiding this comment

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

Or is it decided that it is better to add this just as a placeholder and implement in a separate PR, as originally planned?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, sorry for half-introducing classes from other tasks - I wanted native methods signatures to be ready for native tasks, so created these classes as placeholders

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's keep them as placeholders to avoid redundant work then.


@VisibleForTesting
final static short MAX_SERVICE_NUMBER = 256;
private final static Injector frameworkInjector = Guice.createInjector(new TestKitFrameworkModule());
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private final static Injector frameworkInjector = Guice.createInjector(new TestKitFrameworkModule());
private static final Injector frameworkInjector = Guice.createInjector(new TestKitFrameworkModule());

public final class TestKit {

@VisibleForTesting
final static short MAX_SERVICE_NUMBER = 256;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
final static short MAX_SERVICE_NUMBER = 256;
static final short MAX_SERVICE_NUMBER = 256;


private TestKit(List<Class<? extends ServiceModule>> serviceModules, EmulatedNodeType nodeType,
short validatorCount, @Nullable TimeProvider timeProvider) {
List<UserServiceAdapter> serviceAdapters = new ArrayList<>();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
List<UserServiceAdapter> serviceAdapters = new ArrayList<>();
List<UserServiceAdapter> serviceAdapters = new ArrayList<>(serviceModules.size());

@SuppressWarnings("unchecked")
public <T extends Service> T getService(short serviceId, Class<T> serviceClass) {
Service service = services.get(serviceId);
checkArgument(service.getClass().equals(serviceClass), "Service with id %s is not of expected class %s",
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
checkArgument(service.getClass().equals(serviceClass), "Service with id %s is not of expected class %s",
checkArgument(service.getClass().equals(serviceClass), "Service with id %s is not an instance of expected class %s",

try {
Constructor constructor = moduleClass.getDeclaredConstructor();
Object moduleObject = constructor.newInstance();
checkArgument(moduleObject instanceof Module, "%s is not a sub-class of %s",
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
checkArgument(moduleObject instanceof Module, "%s is not a sub-class of %s",
checkArgument(moduleObject instanceof Module, "%s is not a sub-class of the %s",

Btw, looks like this check is always true because ServiceModule extends Module. No? I believe we can cast to ServiceModule at initialization and remove this check.

Copy link
Contributor

Choose a reason for hiding this comment

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

This code seems to be copied from the place where we instantiate a class and subsequently a module by name, therefore, some checks can be redundant.

I'd also look into multiple-services branch. There might be something reusable here.

Copy link
Contributor

Choose a reason for hiding this comment

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

com.exonum.binding.runtime.ReflectiveModuleSupplier

}

private void checkMaxServiceNumber(List<Class<? extends ServiceModule>> serviceModules) {
checkArgument(serviceModules.size() < MAX_SERVICE_NUMBER,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it allowed to have 255 services, not 256?

Copy link
Contributor

Choose a reason for hiding this comment

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

if the constant is named max, it must be inclusive


import java.time.ZonedDateTime;

public interface TimeProvider {
Copy link
Contributor

Choose a reason for hiding this comment

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

javadoc

Copy link
Contributor

Choose a reason for hiding this comment

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

Or in time-PR?


@Test
void createTestKitWithBuilderForMultipleSameServices() {
List<Class<? extends ServiceModule>> serviceModules = new ArrayList<>();
Copy link
Contributor

Choose a reason for hiding this comment

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

List.of(...) or ImmutableList.of()

Copy link
Contributor

@dmitry-timofeev dmitry-timofeev left a comment

Choose a reason for hiding this comment

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

Generally good, thanks! There are some comments re docs and tests

}

/**
* Returns a validator id if this node is a validator or {@link OptionalInt.EMPTY} is this is a validator node.
Copy link
Contributor

Choose a reason for hiding this comment

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

if an auditor node.

}

/**
* Returns a service key pair of this node.
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if there is a good place to link in this case (Node#getPublicKey — but it only returns the public key;
Node#submitTransaction — it signs a transaction with the service public key; or ValidatorKey#consensusKey — but its
documentation is not particularly detailed).

At least, I'd add here in which operations this key pair is used, e.g., "This key pair is used to sign transactions {@linkplain Node#submitTransaction produced} by the service itself".

}

/**
* Returns a consensus key pair of this node.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd add that this key pair is used to sign consensus message of this node.


import java.time.ZonedDateTime;

public class FakeTimeProvider implements TimeProvider {
Copy link
Contributor

Choose a reason for hiding this comment

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

Or is it decided that it is better to add this just as a placeholder and implement in a separate PR, as originally planned?

/**
* 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 (either validator or auditor) would be truly emulated and execute its
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe specify that a single instance per service is created, e.g.:

"... only one node will create the service instances and will execute their operations (e.g., ...)"

return new TestKit(services, nodeType, validatorCount, timeProvider);
}

private void checkMaxServiceNumber(List<Class<? extends ServiceModule>> serviceModules) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there anything else that must be validated? E.g., non-empty list of services?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added such a check, even though it's possible to create a blockchain without any user services and if users want to run such a network (only with time service, for example), maybe we should let them, even if it's useless? But probably it's better to fail in such case to indicate that a user forgot to add a service


import java.time.ZonedDateTime;

public interface TimeProvider {
Copy link
Contributor

Choose a reason for hiding this comment

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

Or in time-PR?

void createTestKitForSingleService() {
TestKit testKit = TestKit.forService(TestServiceModule.class);
Service service = testKit.getService(TestService.SERVICE_ID, TestService.class);
assertEquals(service.getId(), TestService.SERVICE_ID);
Copy link
Contributor

Choose a reason for hiding this comment

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

Shan't we test the other side-effects of creating a testkit (= service initialization, mounting of web handlers)?
I think we can do that with a service implementation that produces some effects in its #initialize and returns
some value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was planning to add that later, with the native part, rather than writing and ignoring them now. Should I do the latter?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think either is OK, if we don't forget to add the proper tests/update the already added to test all aspects.

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 create a separate task for that then.

void createTestKitWithBuilderForMultipleSameServices() {
List<Class<? extends ServiceModule>> serviceModules = new ArrayList<>();
serviceModules.add(TestServiceModule.class);
serviceModules.add(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.

Shall we allow that? With the present framework version it will not work at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IIRC there was a discussion on wiki some time ago stating that we should, but that could change. What's the current outlook on having several instances of the same service?

Copy link
Contributor

Choose a reason for hiding this comment

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

Even with multiple services, it is not possible to create two instances of the same service because their database schemas will be the same; their ids will be the same (so the testkit will likely fail somewhere in native). Dynamic services will require providing instance parameters — i.e., something on top of a ServiceModule (e.g., a service instance name, or other initialization parameters), therefore the present interface won't work with them.

<url>https://exonum.com/doc/version/latest/get-started/test-service/</url>

<properties>
<ejb-core.nativeLibPath>${project.parent.basedir}/core/rust/target/debug
Copy link
Contributor

Choose a reason for hiding this comment

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

I think since #818 it must be slightly different, @vitvakatu can advise

Copy link
Contributor

Choose a reason for hiding this comment

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

It's okay, as we don't have tests with release mode at the moment

@MakarovS MakarovS added the work-in-progress 👷‍♂️ Do not expect reviewers to provide any feedback on WIP PRs — please ask for it explicitly! label Apr 4, 2019

/**
* Returns a consensus key pair of this node. This key pair is used to sign consensus message of
* this node.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Plural for "transactions" in the comment for .getServiceKeyPair() and singular for "consensus message" here.


@Override
public void createPublicApiHandlers(Node node, Router router) {
throw new UnsupportedOperationException();
Copy link
Contributor

Choose a reason for hiding this comment

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

We always call createPublicApiHandlers, even if no handlers are needed. I wonder if constructing this service will lead to panic

}

/**
* Creates the TestKit instance.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we mention genesis block creation? That's all I believe

@MakarovS MakarovS removed the work-in-progress 👷‍♂️ Do not expect reviewers to provide any feedback on WIP PRs — please ask for it explicitly! label Apr 5, 2019
@coveralls
Copy link

coveralls commented Apr 5, 2019

Coverage Status

Coverage decreased (-0.3%) to 85.742% when pulling 88cbab6 on ECR-3049 into 492d956 on master.

* network, only one node will create the service instances and will execute their operations
* (e.g., {@link Service#afterCommit(BlockCommittedEvent)} method logic).
*
* <p>When TestKit is created, Exonum blockchain instance with given services is initialized and
Copy link
Contributor

Choose a reason for hiding this comment

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

@vitvakatu, Would you clarify please what is the exact order? Do we initialize after
genesis block? When do we create public API handlers?

Copy link
Contributor

Choose a reason for hiding this comment

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

The order is initialize -> genesis block commit -> create handlers. Service::initialize is called on genesis block creation, so actual it looks more like initialize and genesis block commit -> create handlers

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks, it seems this description needs to be updated to reflect the actual order of operations.

}

/**
* Returns an instance of a service with the given service id and service class.
Copy link
Contributor

Choose a reason for hiding this comment

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

+ "Only user-defined services can be requested, i.e., it is not possible to get an instance of a built-in service such as the time oracle."

* Returns an instance of a service with the given service id and service class.
*
* @return the service instance or null if there is no service with such id
* @throws NullPointerException if the service with given id was not found
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 not appropriate to throw NPE when it is not the client passing nulls. IllegalArgumentException would be more appropriate (checkArgument(services.containsKey…)

}

/**
* Creates a user service adapter from the service module.
Copy link
Contributor

Choose a reason for hiding this comment

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

Instantiates a service given its module and wraps in a UserServiceAdapter
for the native code.

(we don't create a USA from the module, but a service, and then wrap it).

/**
* Returns a list of user service adapters created from given service modules.
*/
private List<UserServiceAdapter> toUserServiceAdapters(List<Class<? extends ServiceModule>> serviceModules) {
Copy link
Contributor

Choose a reason for hiding this comment

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

These methods are used by the constructor only, therefore, I'd move them closer to it

  • ctor
    • toUserServiceAdapters
      • createUserServiceAdapter
    • populateServiceMap

Copy link
Contributor

@dmitry-timofeev dmitry-timofeev Apr 5, 2019

Choose a reason for hiding this comment

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

If they are private, have only a single usage and are adjacent to it, some documentation comments might no longer be needed (unless they clarify something possibly unclear).

Copy link
Contributor

Choose a reason for hiding this comment

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

… and checkForDuplicateService obviously

UserServiceAdapter serviceAdapter = createUserServiceAdapter(moduleClass);
serviceAdapters.add(serviceAdapter);
}
return serviceAdapters;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return serviceAdapters;
return serviceModules.stream()
.map(this::createUserServiceAdapter)
.collect(Collectors.toList());

Copy link
Contributor

Choose a reason for hiding this comment

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

with static imports, probably, as @bullet-tooth would recommend :-)

}
}

private void checkForDuplicateService(short serviceId) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private void checkForDuplicateService(short serviceId) {
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());
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Otherwise it might not be clear which, ids are just numbers

}

/**
* Adds a variable number of services with which the TestKit would be instantiated.
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: omit the type (below it is no longer correct as any iterable is allowed, not just list): "Adds services with which the TestKit would be instantiated."

/**
* Adds a variable number of services with which the TestKit would be instantiated.
*/
public Builder withServices(Class<? extends ServiceModule> serviceModule,
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems to qualify for @SafeVarargs, but double-check that

* Adds a list of services with which the TestKit would be instantiated.
*/
public Builder withServices(Iterable<Class<? extends ServiceModule>> serviceModules) {
serviceModules.forEach(services::add);
Copy link
Contributor

Choose a reason for hiding this comment

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

I find this one a little clearer (addAll instead of generic for each):

Suggested change
serviceModules.forEach(services::add);
Iterables.addAll(services, serviceModules);

Class<? extends ServiceModule>... serviceModules) {
List<Class<? extends ServiceModule>> services = asList(serviceModule, serviceModules);
this.services.addAll(services);
return this;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return this;
return withServices(asList(serviceModule, serviceModules));

Copy link
Contributor

@bullet-tooth bullet-tooth left a comment

Choose a reason for hiding this comment

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

👍

* regardless of the configured number of validators, only a single service will be
* instantiated.
*/
public final Builder withValidators(short validatorCount) {
Copy link
Contributor

Choose a reason for hiding this comment

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

final is redundant for methods such as the class is final.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, I think it is only required (even in final classes) for @SafeVarargs methods.

Copy link
Contributor Author

@MakarovS MakarovS Apr 9, 2019

Choose a reason for hiding this comment

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

Yes, it was needed for @SafeVarargs method, so I added others for consistency.
Edit: removed for all except for @SafeVarargs method

* Adds services with which the TestKit would be instantiated.
*/
@SafeVarargs
public final Builder withServices(Class<? extends ServiceModule> serviceModule,
Copy link
Contributor

Choose a reason for hiding this comment

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

actually, serviceModule param can be removed and java.util.Arrays#asList is used as the varargs wrapper. Or did I misunderstand something?)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, it does not seem to be needed for resolution of methods with ambiguious signatures (the one above has different name). However, it does communicate a requirement of at least one service module.

Copy link
Contributor

Choose a reason for hiding this comment

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

a requirement of at least one service module
There is withService in this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are there any disadvantages to the present signature? If not, I'd leave it as it is


static final class TestService implements Service {

static short SERVICE_ID = 42;
Copy link
Contributor

Choose a reason for hiding this comment

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

Cryptocurrency service has id=42 as well. But I think it's not a problem.

Copy link
Contributor Author

@MakarovS MakarovS Apr 9, 2019

Choose a reason for hiding this comment

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

Yes, I thought about that, but that shouldn't be a problem
Edit: changed anyway :)

@MakarovS
Copy link
Contributor Author

MakarovS commented Apr 9, 2019

It appears that in native we can't get a private consensus key, so after discussion with @vitvakatu we decided to remove consensus keys altogether (even though the public key is available), as it seems consensus keys would be not necessary for users. But we could give access to consensus public key or even change the core and make it possible to access both public and private consensus keys. Does anyone sees any usefulness in those? @dmitry-timofeev @bullet-tooth

@dmitry-timofeev
Copy link
Contributor

I don't see immediate usefulness in the consensus public key access. You can validate precommits and other consensus messages with it — but why would you?

@dmitry-timofeev dmitry-timofeev merged commit 4124c8d into master Apr 10, 2019
@MakarovS MakarovS deleted the ECR-3049 branch April 12, 2019 05:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

6 participants