-
Notifications
You must be signed in to change notification settings - Fork 30
Add TestKit [ECR-3049] #819
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
package com.exonum.binding.testkit; | ||
|
||
/** | ||
* Type of the TestKit emulated 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.
/**
* 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 { |
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.
javadoc is missing. Please describe the purpose of this 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.
Or is it decided that it is better to add this just as a placeholder and implement in a separate PR, as originally planned?
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.
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
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.
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()); |
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.
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; |
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.
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<>(); |
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.
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", |
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.
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", |
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.
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.
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 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.
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.
com.exonum.binding.runtime.ReflectiveModuleSupplier
} | ||
|
||
private void checkMaxServiceNumber(List<Class<? extends ServiceModule>> serviceModules) { | ||
checkArgument(serviceModules.size() < MAX_SERVICE_NUMBER, |
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.
Is it allowed to have 255 services, not 256?
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.
if the constant is named max, it must be inclusive
|
||
import java.time.ZonedDateTime; | ||
|
||
public interface TimeProvider { |
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.
javadoc
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 in time-PR?
|
||
@Test | ||
void createTestKitWithBuilderForMultipleSameServices() { | ||
List<Class<? extends ServiceModule>> serviceModules = new ArrayList<>(); |
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.
List.of(...)
or ImmutableList.of()
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.
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. |
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.
if an auditor node.
} | ||
|
||
/** | ||
* Returns a service key pair of this 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.
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. |
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 that this key pair is used to sign consensus message of this node.
|
||
import java.time.ZonedDateTime; | ||
|
||
public class FakeTimeProvider implements TimeProvider { |
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 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 |
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.
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) { |
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.
Is there anything else that must be validated? E.g., non-empty list of services?
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 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 { |
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 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); |
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.
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.
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 was planning to add that later, with the native part, rather than writing and ignoring them now. Should I do the latter?
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 think either is OK, if we don't forget to add the proper tests/update the already added to test all aspects.
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 create a separate task for that then.
void createTestKitWithBuilderForMultipleSameServices() { | ||
List<Class<? extends ServiceModule>> serviceModules = new ArrayList<>(); | ||
serviceModules.add(TestServiceModule.class); | ||
serviceModules.add(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.
Shall we allow that? With the present framework version it will not work at all.
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.
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?
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.
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 |
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 think since #818 it must be slightly different, @vitvakatu can advise
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's okay, as we don't have tests with release mode at the moment
|
||
/** | ||
* Returns a consensus key pair of this node. This key pair is used to sign consensus message of | ||
* this 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.
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(); |
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 always call createPublicApiHandlers, even if no handlers are needed. I wonder if constructing this service will lead to panic
} | ||
|
||
/** | ||
* Creates the TestKit instance. |
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.
Should we mention genesis block creation? That's all I believe
* 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 |
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.
@vitvakatu, Would you clarify please what is the exact order? Do we initialize after
genesis block? When do we create public API handlers?
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 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
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.
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. |
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.
+ "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 |
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 not appropriate to throw NPE when it is not the client passing null
s. IllegalArgumentException
would be more appropriate (checkArgument(services.containsKey…
)
} | ||
|
||
/** | ||
* Creates a user service adapter from the service module. |
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.
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) { |
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.
These methods are used by the constructor only, therefore, I'd move them closer to it
- ctor
- toUserServiceAdapters
- createUserServiceAdapter
- populateServiceMap
- toUserServiceAdapters
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.
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).
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 checkForDuplicateService
obviously
UserServiceAdapter serviceAdapter = createUserServiceAdapter(moduleClass); | ||
serviceAdapters.add(serviceAdapter); | ||
} | ||
return 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.
return serviceAdapters; | |
return serviceModules.stream() | |
.map(this::createUserServiceAdapter) | |
.collect(Collectors.toList()); |
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.
with static imports, probably, as @bullet-tooth would recommend :-)
} | ||
} | ||
|
||
private void checkForDuplicateService(short serviceId) { |
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.
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()); | |
} |
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.
Otherwise it might not be clear which, ids are just numbers
} | ||
|
||
/** | ||
* Adds a variable number of services with which the TestKit would be instantiated. |
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: 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, |
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 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); |
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 find this one a little clearer (addAll instead of generic for each):
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; |
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.
return this; | |
return withServices(asList(serviceModule, serviceModules)); |
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.
👍
* regardless of the configured number of validators, only a single service will be | ||
* instantiated. | ||
*/ | ||
public final Builder withValidators(short validatorCount) { |
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.
final
is redundant for methods such as the class is final.
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.
Yep, I think it is only required (even in final classes) for @SafeVarargs methods.
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.
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, |
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.
actually, serviceModule
param can be removed and java.util.Arrays#asList
is used as the varargs wrapper. Or did I misunderstand something?)
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.
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.
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.
a requirement of at least one service module
There iswithService
in this case.
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.
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; |
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.
Cryptocurrency service has id=42 as well. But I think it's not a problem.
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.
Yes, I thought about that, but that shouldn't be a problem
Edit: changed anyway :)
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 |
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? |
Overview
Add TestKit P1 functionality.
See: https://jira.bf.local/browse/ECR-3049
Definition of Done