Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,20 @@
import com.exonum.binding.proxy.CloseFailuresException;
import com.exonum.binding.storage.database.MemoryDb;
import com.exonum.binding.storage.database.Snapshot;
import com.exonum.binding.storage.database.View;
import com.exonum.binding.test.RequiresNativeLibrary;
import com.exonum.binding.util.LibraryLoader;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@RequiresNativeLibrary
@ExtendWith(ViewTestExtension.class)
class CoreSchemaProxyIntegrationTest {

static {
LibraryLoader.load();
@BeforeEach
void setUp(MemoryDb db) {
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 just for illustrative purposes, i.e., it's not needed for the below test to work, is it?

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, it's not needed, just a demonstration that we can inject parameters into @BeforeEach as well

assertThat(db).isNotNull();
}

@Test
Expand All @@ -57,15 +62,10 @@ void getAllBlockHashesTest() throws CloseFailuresException {
}

@Test
void getBlockTransactionsTest() throws CloseFailuresException {
try (MemoryDb db = MemoryDb.newInstance();
Cleaner cleaner = new Cleaner()) {
Snapshot view = db.createSnapshot(cleaner);

CoreSchemaProxy schema = CoreSchemaProxy.newInstance(view);
long height = 0L;
assertThat(schema.getBlockTransactions(height)).isEmpty();
}
void getBlockTransactionsTest(View view) {
CoreSchemaProxy schema = CoreSchemaProxy.newInstance(view);
long height = 0L;
assertThat(schema.getBlockTransactions(height)).isEmpty();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright 2018 The Exonum Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.exonum.binding.blockchain;

import com.exonum.binding.proxy.Cleaner;
import com.exonum.binding.storage.database.Fork;
import com.exonum.binding.storage.database.MemoryDb;
import com.exonum.binding.storage.database.Snapshot;
import com.exonum.binding.storage.database.View;
import com.exonum.binding.util.LibraryLoader;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;

/**
* This extension allows to easily use Views in tests.
*
* After annotating the test class with <code>@ExtendWith(ViewTestExtension.class)</code> user is
* able to write tests like this:
*
* <pre><code>
* @BeforeEach
* void setUp(MemoryDb db) {
* // Use db if needed
* }
*
* @Test
* void test(View view) {
* // Test logic
* }
* </code></pre>
*
* instead of:
*
* <pre><code>
* static {
* LibraryLoader.load();
* }
*
* MemoryDb db;
*
* @BeforeEach
* void setUp() {
* db = MemoryDb.newInstance();
* // Use db if needed
* }
*
* @Test
* void test() {
* try (MemoryDb db = MemoryDb.newInstance();
* Cleaner cleaner = new Cleaner()) {
* View view = db.createSnapshot(cleaner);
* // Test logic
* }
* }
* </code></pre>
*/
public class ViewTestExtension implements ParameterResolver, BeforeEachCallback {

private static final String KEY = "ResourceKey";

static {
LibraryLoader.load();
}

@Override
public void beforeEach(ExtensionContext extensionContext) throws Exception {
MemoryDb db = MemoryDb.newInstance();
Cleaner cleaner = new Cleaner();
getStore(extensionContext).put(KEY, new CloseableDbAndCleaner(db, cleaner));
}

@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.getParameter().getType() == MemoryDb.class
|| parameterContext.getParameter().getType() == View.class
|| parameterContext.getParameter().getType() == Snapshot.class
|| parameterContext.getParameter().getType() == Fork.class;
}

@Override
public Object resolveParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
CloseableDbAndCleaner resources =
getStore(extensionContext).get(KEY, CloseableDbAndCleaner.class);
MemoryDb memoryDb = resources.getMemoryDb();
if (parameterContext.getParameter().getType() == MemoryDb.class) {
return memoryDb;
}
else {
Cleaner cleaner = resources.getCleaner();
Copy link
Contributor

Choose a reason for hiding this comment

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

This can probably simplify things too (with hierarchical store), as before each callback will no longer be needed:

MemoryDb memoryDb = store.getOrComputeIfAbsent(MEMORY_DB_KEY, CloseableMemoryDb::new);
if (requestedType == MemoryDb.class) {
  return memoryDb;
}
// …

It seems natural to allow to inject the same memoryDb as in setup method (which merges some data), but shall we also allow reusing snapshots and forks in lower levels of the lifecycle hierarchy? If we do not, which seems like a reasonable behaviour, then how do we destroy a cleaner exactly after the setUp?

if (parameterContext.getParameter().getType() == Fork.class) {
return memoryDb.createFork(cleaner);
} else {
return memoryDb.createSnapshot(cleaner);
}
}
}

private Store getStore(ExtensionContext context) {
return context.getStore(Namespace.create(getClass(), context.getRequiredTestMethod()));
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't this code use just a single constant namespace, because the store is hierarchical:

- FooTest
  - setUp(MemoryDb) <- If we resolve MemoryDb here to instance A, 
    - test1(MemoryDb) <- … the same instance A will be accessible here
    - test2(Snapshot) <- … and this snapshot must come from the same instance A too
- BarTest
  - test3(MemoryDb, Fork) <- Has a distinct store from FooTest, hence will create a new MemoryDb instance B and get a fork from it.

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, we could do it either way

}

private static class CloseableDbAndCleaner implements CloseableResource {
Copy link
Contributor

Choose a reason for hiding this comment

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

It probably makes sense to bind a Fork/Snapshot to a Cleaner, not a database.


private final MemoryDb memoryDb;
private final Cleaner cleaner;

CloseableDbAndCleaner(MemoryDb memoryDb, Cleaner cleaner) {
this.memoryDb = memoryDb;
this.cleaner = cleaner;
}

MemoryDb getMemoryDb() {
return memoryDb;
}

Cleaner getCleaner() {
return cleaner;
}

@Override
public void close() throws Throwable {
cleaner.close();
}

}

}