Merge remote-tracking branch 'open-source/master' into thomas-merge-3d50e73271cb3bbd95822575349995c9771729dd

This commit is contained in:
Thomas Schroeter 2018-05-16 15:30:46 +01:00
commit 23a0a66128
24 changed files with 374 additions and 92 deletions

View File

@ -64,7 +64,7 @@ buildscript {
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
ext.fileupload_version = '1.3.3'
ext.junit_version = '4.12'
ext.mockito_version = '2.10.0'
ext.mockito_version = '2.18.3'
ext.jopt_simple_version = '5.0.2'
ext.jansi_version = '1.14'
ext.hibernate_version = '5.2.6.Final'

View File

@ -139,8 +139,7 @@ The Dockerform task
The ``Dockerform`` is a sister task of ``Cordform``. It has nearly the same syntax and produces very
similar results - enhanced by an extra file to enable easy spin up of nodes using ``docker-compose``.
Below you can find the example task from the ``IRS Demo<https://github.com/corda/corda/blob/release-V3.0/samples/irs-demo/cordapp/build.gradle#L111>``
included in the samples directory of main Corda GitHub repository:
Below you can find the example task from the `IRS Demo <https://github.com/corda/corda/blob/release-V3.0/samples/irs-demo/cordapp/build.gradle#L111>`_ included in the samples directory of main Corda GitHub repository:
.. sourcecode:: groovy

View File

@ -26,7 +26,7 @@ import static java.util.Collections.emptyList;
import static net.corda.finance.Currencies.DOLLARS;
import static net.corda.finance.Currencies.issuedBy;
import static net.corda.testing.node.NodeTestUtils.transaction;
import static net.corda.testing.internal.InternalTestUtilsKt.rigorousMock;
import static net.corda.testing.internal.RigorousMockKt.rigorousMock;
import static net.corda.testing.core.TestConstants.DUMMY_NOTARY_NAME;
import static org.mockito.Mockito.doReturn;

View File

@ -16,7 +16,6 @@ import net.corda.cordform.CordformNode
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.internal.size
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.KeyManagementService
import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
@ -47,7 +46,7 @@ class NodeInfoWatcherTest {
val tempFolder = TemporaryFolder()
private val scheduler = TestScheduler()
private val testSubscriber = TestSubscriber<NodeInfo>()
private val testSubscriber = TestSubscriber<NodeInfoUpdate>()
private lateinit var nodeInfoAndSigned: NodeInfoAndSigned
private lateinit var nodeInfoPath: Path
@ -111,7 +110,7 @@ class NodeInfoWatcherTest {
try {
val readNodes = testSubscriber.onNextEvents.distinct()
assertEquals(1, readNodes.size)
assertEquals(nodeInfoAndSigned.nodeInfo, readNodes.first())
assertEquals(nodeInfoAndSigned.nodeInfo, (readNodes.first() as? NodeInfoUpdate.Add)?.nodeInfo)
} finally {
subscription.unsubscribe()
}
@ -136,7 +135,7 @@ class NodeInfoWatcherTest {
testSubscriber.awaitValueCount(1, 5, TimeUnit.SECONDS)
// The same folder can be reported more than once, so take unique values.
val readNodes = testSubscriber.onNextEvents.distinct()
assertEquals(nodeInfoAndSigned.nodeInfo, readNodes.first())
assertEquals(nodeInfoAndSigned.nodeInfo, (readNodes.first() as? NodeInfoUpdate.Add)?.nodeInfo)
} finally {
subscription.unsubscribe()
}

View File

@ -916,10 +916,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
}
val subject = CordaX500Name.build(certificates[0].subjectX500Principal)
// TODO Include the name of the distributed notary, which the node is part of, in the notary config so that we
// can cross-check the identity we get from the key store
if (singleName != null && subject != singleName) {
throw ConfigurationException("The name '$singleName' for $id doesn't match what's in the key store: $subject")
} else if (notaryConfig != null && notaryConfig.isClusterConfig && notaryConfig.serviceLegalName != null && subject != notaryConfig.serviceLegalName) {
// Note that we're not checking if `notaryConfig.serviceLegalName` is not present for backwards compatibility.
throw ConfigurationException("The name of the notary service '${notaryConfig.serviceLegalName}' for $id doesn't match what's in the key store: $subject. "+
"You might need to adjust the configuration of `notary.serviceLegalName`.")
}
val certPath = X509Utilities.buildCertPath(certificates)

View File

@ -108,7 +108,8 @@ data class NotaryConfig(val validating: Boolean,
val raft: RaftConfig? = null,
val bftSMaRt: BFTSMaRtConfiguration? = null,
val custom: Boolean = false,
val mysql: MySQLConfiguration? = null
val mysql: MySQLConfiguration? = null,
val serviceLegalName: CordaX500Name? = null
) {
init {
require(raft == null || bftSMaRt == null || !custom || mysql == null) {

View File

@ -445,7 +445,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
deliver(cordaMessage, artemisMessage)
} else {
log.trace { "Discard duplicate message ${cordaMessage.uniqueMessageId} for ${cordaMessage.topic}" }
artemisMessage.individualAcknowledge()
messagingExecutor!!.acknowledge(artemisMessage)
}
}
}

View File

@ -73,7 +73,17 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
fun subscribeToNetworkMap() {
require(fileWatcherSubscription == null) { "Should not call this method twice." }
// Subscribe to file based networkMap
fileWatcherSubscription = fileWatcher.nodeInfoUpdates().subscribe(networkMapCache::addNode)
fileWatcherSubscription = fileWatcher.nodeInfoUpdates().subscribe {
when (it) {
is NodeInfoUpdate.Add -> {
networkMapCache.addNode(it.nodeInfo)
}
is NodeInfoUpdate.Remove -> {
val nodeInfo = networkMapCache.getNodeByHash(it.hash)
nodeInfo?.let { networkMapCache.removeNode(it) }
}
}
}
if (networkMapClient == null) return

View File

@ -36,13 +36,18 @@ import java.time.Duration
import java.util.concurrent.TimeUnit
import kotlin.streams.toList
sealed class NodeInfoUpdate {
data class Add(val nodeInfo: NodeInfo) : NodeInfoUpdate()
data class Remove(val hash: SecureHash) : NodeInfoUpdate()
}
/**
* Class containing the logic to
* - Serialize and de-serialize a [NodeInfo] to disk and reading it back.
* - Poll a directory for new serialized [NodeInfo]
*
* @param nodePath the base path of a node.
* @param pollInterval how often to poll the filesystem in milliseconds. Must be longer then 5 seconds.
* @param pollInterval how often to poll the filesystem in milliseconds. Must be longer than 5 seconds.
* @param scheduler a [Scheduler] for the rx [Observable] returned by [nodeInfoUpdates], this is mainly useful for
* testing. It defaults to the io scheduler which is the appropriate value for production uses.
*/
@ -68,10 +73,10 @@ class NodeInfoWatcher(private val nodePath: Path,
}
}
internal data class NodeInfoFromFile(val nodeInfohash: SecureHash, val lastModified: FileTime)
private val nodeInfosDir = nodePath / CordformNode.NODE_INFO_DIRECTORY
private val nodeInfoFiles = HashMap<Path, FileTime>()
private val _processedNodeInfoHashes = HashSet<SecureHash>()
val processedNodeInfoHashes: Set<SecureHash> get() = _processedNodeInfoHashes
private val nodeInfoFilesMap = HashMap<Path, NodeInfoFromFile>()
val processedNodeInfoHashes: Set<SecureHash> get() = nodeInfoFilesMap.values.map { it.nodeInfohash }.toSet()
init {
require(pollInterval >= 5.seconds) { "Poll interval must be 5 seconds or longer." }
@ -85,33 +90,31 @@ class NodeInfoWatcher(private val nodePath: Path,
* We simply list the directory content every 5 seconds, the Java implementation of WatchService has been proven to
* be unreliable on MacOs and given the fairly simple use case we have, this simple implementation should do.
*
* @return an [Observable] returning [NodeInfo]s, at most one [NodeInfo] is returned for each processed file.
* @return an [Observable] returning [NodeInfoUpdate]s, at most one [NodeInfo] is returned for each processed file.
*/
fun nodeInfoUpdates(): Observable<NodeInfo> {
fun nodeInfoUpdates(): Observable<NodeInfoUpdate> {
return Observable.interval(pollInterval.toMillis(), TimeUnit.MILLISECONDS, scheduler)
.flatMapIterable { loadFromDirectory() }
}
// TODO This method doesn't belong in this class
fun saveToFile(nodeInfoAndSigned: NodeInfoAndSigned) {
return Companion.saveToFile(nodePath, nodeInfoAndSigned)
}
private fun loadFromDirectory(): List<NodeInfo> {
private fun loadFromDirectory(): List<NodeInfoUpdate> {
val processedPaths = HashSet<Path>()
val result = nodeInfosDir.list { paths ->
paths
.filter { it.isRegularFile() }
.filter { file ->
val lastModifiedTime = file.lastModifiedTime()
val previousLastModifiedTime = nodeInfoFiles[file]
val previousLastModifiedTime = nodeInfoFilesMap[file]?.lastModified
val newOrChangedFile = previousLastModifiedTime == null || lastModifiedTime > previousLastModifiedTime
nodeInfoFiles[file] = lastModifiedTime
processedPaths.add(file)
newOrChangedFile
}
.mapNotNull { file ->
logger.debug { "Reading SignedNodeInfo from $file" }
try {
NodeInfoAndSigned(file.readObject())
val nodeInfoSigned = NodeInfoAndSigned(file.readObject())
nodeInfoFilesMap[file] = NodeInfoFromFile(nodeInfoSigned.signed.raw.hash, file.lastModifiedTime())
nodeInfoSigned
} catch (e: Exception) {
logger.warn("Unable to read SignedNodeInfo from $file", e)
null
@ -119,10 +122,13 @@ class NodeInfoWatcher(private val nodePath: Path,
}
.toList()
}
val removedFiles = nodeInfoFilesMap.keys - processedPaths
val removedHashes = removedFiles.map { file ->
NodeInfoUpdate.Remove(nodeInfoFilesMap.remove(file)!!.nodeInfohash)
}
logger.debug { "Read ${result.size} NodeInfo files from $nodeInfosDir" }
_processedNodeInfoHashes += result.map { it.signed.raw.hash }
return result.map { it.nodeInfo }
logger.debug { "Number of removed NodeInfo files ${removedHashes.size}" }
return result.map { NodeInfoUpdate.Add(it.nodeInfo) } + removedHashes
}
}

View File

@ -262,8 +262,8 @@ open class PersistentNetworkMapCache(
}
private fun removeInfoDB(session: Session, nodeInfo: NodeInfo) {
val info = findByIdentityKey(session, nodeInfo.legalIdentitiesAndCerts.first().owningKey).single()
session.remove(info)
val info = findByIdentityKey(session, nodeInfo.legalIdentitiesAndCerts.first().owningKey).singleOrNull()
info?.let { session.remove(it) }
// invalidate cache last - this way, we might serve up the wrong info for a short time, but it will get refreshed
// on the next load
invalidateCaches(nodeInfo)

View File

@ -60,7 +60,7 @@ import static net.corda.core.node.services.vault.Builder.sum;
import static net.corda.core.node.services.vault.QueryCriteriaUtils.*;
import static net.corda.core.utilities.ByteArrays.toHexString;
import static net.corda.testing.core.TestConstants.*;
import static net.corda.testing.internal.InternalTestUtilsKt.rigorousMock;
import static net.corda.testing.internal.RigorousMockKt.rigorousMock;
import static net.corda.testing.node.MockServices.makeTestDatabaseAndMockServices;
import static net.corda.testing.node.MockServicesKt.makeTestIdentityService;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -18,7 +18,6 @@ import net.corda.core.flows.FlowLogicRef
import net.corda.core.flows.FlowLogicRefFactory
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.uncheckedCast
import net.corda.core.node.ServicesForResolution
import net.corda.core.utilities.days
import net.corda.node.internal.configureDatabase
@ -30,6 +29,7 @@ import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
import net.corda.testing.internal.doLookup
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.spectator
import net.corda.testing.node.MockServices
import net.corda.testing.node.TestClock
import org.junit.Ignore
@ -54,16 +54,9 @@ open class NodeSchedulerServiceTestBase {
protected val testClock = TestClock(rigorousMock<Clock>().also {
doReturn(mark).whenever(it).instant()
})
private val database = rigorousMock<CordaPersistence>().also {
doAnswer {
val block: DatabaseTransaction.() -> Any? = uncheckedCast(it.arguments[0])
rigorousMock<DatabaseTransaction>().block()
}.whenever(it).transaction(any())
}
protected val flowStarter = rigorousMock<FlowStarter>().also {
doAnswer {
val dedupe = it.arguments[2] as DeduplicationHandler
val dedupe: DeduplicationHandler = it.getArgument(2)
dedupe.insideDatabaseTransaction()
dedupe.afterDatabaseTransaction()
openFuture<FlowStateMachine<*>>()
@ -84,11 +77,8 @@ open class NodeSchedulerServiceTestBase {
protected val servicesForResolution = rigorousMock<ServicesForResolution>().also {
doLookup(transactionStates).whenever(it).loadState(any())
}
protected val log = rigorousMock<Logger>().also {
protected val log = spectator<Logger>().also {
doReturn(false).whenever(it).isTraceEnabled
doNothing().whenever(it).trace(any(), any<Any>())
doNothing().whenever(it).info(any())
doNothing().whenever(it).error(any(), any<Throwable>())
}
protected fun assertWaitingFor(ssr: ScheduledStateRef, total: Int = 1) {
@ -100,7 +90,7 @@ open class NodeSchedulerServiceTestBase {
protected fun assertStarted(flowLogic: FlowLogic<*>) {
// Like in assertWaitingFor, use timeout to make verify wait as we often race the call to startFlow:
verify(flowStarter, timeout(5000)).startFlow(same(flowLogic)!!, any(), any())
verify(flowStarter, timeout(5000)).startFlow(same(flowLogic), any(), any())
}
protected fun assertStarted(event: Event) = assertStarted(event.flowLogic)
@ -134,7 +124,7 @@ class MockScheduledFlowRepository : ScheduledFlowRepository {
class NodeSchedulerServiceTest : NodeSchedulerServiceTestBase() {
private val database = rigorousMock<CordaPersistence>().also {
doAnswer {
val block: DatabaseTransaction.() -> Any? = uncheckedCast(it.arguments[0])
val block: DatabaseTransaction.() -> Any? = it.getArgument(0)
rigorousMock<DatabaseTransaction>().block()
}.whenever(it).transaction(any())
}
@ -164,7 +154,7 @@ class NodeSchedulerServiceTest : NodeSchedulerServiceTestBase() {
val logicRef = rigorousMock<FlowLogicRef>()
transactionStates[stateRef] = rigorousMock<TransactionState<SchedulableState>>().also {
doReturn(rigorousMock<SchedulableState>().also {
doReturn(ScheduledActivity(logicRef, time)).whenever(it).nextScheduledActivity(same(stateRef)!!, any())
doReturn(ScheduledActivity(logicRef, time)).whenever(it).nextScheduledActivity(same(stateRef), any())
}).whenever(it).data
}
flows[logicRef] = flowLogic

View File

@ -15,6 +15,7 @@ import com.google.common.jimfs.Jimfs
import com.nhaarman.mockito_kotlin.*
import net.corda.cordform.CordformNode.NODE_INFO_DIRECTORY
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.sign
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.*
@ -29,6 +30,7 @@ import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.*
import net.corda.testing.driver.PortAllocation
import net.corda.testing.internal.DEV_ROOT_CA
import net.corda.testing.internal.TestNodeInfoBuilder
import net.corda.testing.internal.createNodeInfoAndSigned
import net.corda.testing.node.internal.network.NetworkMapServer
import org.assertj.core.api.Assertions
@ -56,6 +58,7 @@ class NetworkMapUpdaterTest {
private val privateNetUUID = UUID.randomUUID()
private val fs = Jimfs.newFileSystem(unix())
private val baseDir = fs.getPath("/node")
private val nodeInfoDir = baseDir / NODE_INFO_DIRECTORY
private val scheduler = TestScheduler()
private val fileWatcher = NodeInfoWatcher(baseDir, scheduler)
private val networkMapCache = createMockNetworkMapCache()
@ -100,7 +103,7 @@ class NetworkMapUpdaterTest {
verify(networkMapCache, times(1)).addNode(nodeInfo1)
verify(networkMapCache, times(1)).addNode(nodeInfo2)
NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, fileNodeInfoAndSigned)
NodeInfoWatcher.saveToFile(nodeInfoDir, fileNodeInfoAndSigned)
networkMapClient.publish(signedNodeInfo3)
networkMapClient.publish(signedNodeInfo4)
scheduler.advanceTimeBy(10, TimeUnit.SECONDS)
@ -122,7 +125,7 @@ class NetworkMapUpdaterTest {
val fileNodeInfoAndSigned = createNodeInfoAndSigned("Info from file")
// Add all nodes.
NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, fileNodeInfoAndSigned)
NodeInfoWatcher.saveToFile(nodeInfoDir, fileNodeInfoAndSigned)
networkMapClient.publish(signedNodeInfo1)
networkMapClient.publish(signedNodeInfo2)
networkMapClient.publish(signedNodeInfo3)
@ -162,7 +165,7 @@ class NetworkMapUpdaterTest {
updater.subscribeToNetworkMap()
NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, fileNodeInfoAndSigned)
NodeInfoWatcher.saveToFile(nodeInfoDir, fileNodeInfoAndSigned)
scheduler.advanceTimeBy(10, TimeUnit.SECONDS)
verify(networkMapCache, times(1)).addNode(any())
@ -225,17 +228,72 @@ class NetworkMapUpdaterTest {
assertEquals(aliceInfo, networkMapClient.getNodeInfo(aliceHash))
}
@Test
fun `remove node from filesystem deletes it from network map cache`() {
val fileNodeInfoAndSigned1 = createNodeInfoAndSigned("Info from file 1")
val fileNodeInfoAndSigned2 = createNodeInfoAndSigned("Info from file 2")
updater.subscribeToNetworkMap()
NodeInfoWatcher.saveToFile(nodeInfoDir, fileNodeInfoAndSigned1)
NodeInfoWatcher.saveToFile(nodeInfoDir, fileNodeInfoAndSigned2)
scheduler.advanceTimeBy(10, TimeUnit.SECONDS)
verify(networkMapCache, times(2)).addNode(any())
verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned1.nodeInfo)
verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned2.nodeInfo)
assertThat(networkMapCache.allNodeHashes).containsExactlyInAnyOrder(fileNodeInfoAndSigned1.signed.raw.hash, fileNodeInfoAndSigned2.signed.raw.hash)
// Remove one of the nodes
val fileName1 = "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}${fileNodeInfoAndSigned1.nodeInfo.legalIdentities[0].name.serialize().hash}"
(nodeInfoDir / fileName1).delete()
scheduler.advanceTimeBy(10, TimeUnit.SECONDS)
verify(networkMapCache, times(1)).removeNode(any())
verify(networkMapCache, times(1)).removeNode(fileNodeInfoAndSigned1.nodeInfo)
assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfoAndSigned2.signed.raw.hash)
}
@Test
fun `remove node info file, but node in network map server`() {
val nodeInfoBuilder = TestNodeInfoBuilder()
val (_, key) = nodeInfoBuilder.addLegalIdentity(CordaX500Name("Info", "London", "GB"))
val (serverNodeInfo, serverSignedNodeInfo) = nodeInfoBuilder.buildWithSigned(1, 1)
// Construct node for exactly same identity, but different serial. This one will go to additional-node-infos only.
val localNodeInfo = serverNodeInfo.copy(serial = 17)
val localSignedNodeInfo = NodeInfoAndSigned(localNodeInfo) { _, serialised ->
key.sign(serialised.bytes)
}
// The one with higher serial goes to additional-node-infos.
NodeInfoWatcher.saveToFile(nodeInfoDir, localSignedNodeInfo)
// Publish to network map the one with lower serial.
networkMapClient.publish(serverSignedNodeInfo)
updater.subscribeToNetworkMap()
scheduler.advanceTimeBy(10, TimeUnit.SECONDS)
verify(networkMapCache, times(1)).addNode(localNodeInfo)
Thread.sleep(2L * cacheExpiryMs)
// Node from file has higher serial than the one from NetworkMapServer
assertThat(networkMapCache.allNodeHashes).containsOnly(localSignedNodeInfo.signed.raw.hash)
val fileName = "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}${localNodeInfo.legalIdentities[0].name.serialize().hash}"
(nodeInfoDir / fileName).delete()
scheduler.advanceTimeBy(10, TimeUnit.SECONDS)
verify(networkMapCache, times(1)).removeNode(any())
verify(networkMapCache).removeNode(localNodeInfo)
Thread.sleep(2L * cacheExpiryMs)
// Instead of node from file we should have now the one from NetworkMapServer
assertThat(networkMapCache.allNodeHashes).containsOnly(serverSignedNodeInfo.raw.hash)
}
private fun createMockNetworkMapCache(): NetworkMapCacheInternal {
return mock {
val data = ConcurrentHashMap<Party, NodeInfo>()
on { addNode(any()) }.then {
val nodeInfo = it.arguments[0] as NodeInfo
data.put(nodeInfo.legalIdentities[0], nodeInfo)
val party = nodeInfo.legalIdentities[0]
data.compute(party) { _, current ->
if (current == null || current.serial < nodeInfo.serial) nodeInfo else current
}
}
on { removeNode(any()) }.then { data.remove((it.arguments[0] as NodeInfo).legalIdentities[0]) }
on { getNodeByLegalIdentity(any()) }.then { data[it.arguments[0]] }
on { allNodeHashes }.then { data.values.map { it.serialize().hash } }
on { getNodeByHash(any()) }.then { mock -> data.values.single { it.serialize().hash == mock.arguments[0] } }
on { getNodeByHash(any()) }.then { mock -> data.values.singleOrNull { it.serialize().hash == mock.arguments[0] } }
}
}

View File

@ -29,6 +29,7 @@ ext['hibernate.version'] = "$hibernate_version"
ext['selenium.version'] = "$selenium_version"
ext['jackson.version'] = "$jackson_version"
ext['dropwizard-metrics.version'] = "$metrics_version"
ext['mockito.version'] = "$mockito_version"
apply plugin: 'java'
apply plugin: 'kotlin'
@ -124,4 +125,4 @@ idea {
downloadJavadoc = true // defaults to false
downloadSources = true
}
}
}

View File

@ -30,7 +30,7 @@ ext['artemis.version'] = "$artemis_version"
ext['hibernate.version'] = "$hibernate_version"
ext['jackson.version'] = "$jackson_version"
ext['dropwizard-metrics.version'] = "$metrics_version"
ext['mockito.version'] = "$mockito_version"
apply plugin: 'java'
apply plugin: 'kotlin'

View File

@ -58,7 +58,7 @@ class BFTNotaryCordform : CordformDefinition() {
val clusterAddresses = (0 until clusterSize).map { NetworkHostAndPort("localhost", 11000 + it * 10) }
fun notaryNode(replicaId: Int, configure: CordformNode.() -> Unit) = node {
name(notaryNames[replicaId])
notary(NotaryConfig(validating = false, bftSMaRt = BFTSMaRtConfiguration(replicaId, clusterAddresses)))
notary(NotaryConfig(validating = false, serviceLegalName = clusterName, bftSMaRt = BFTSMaRtConfiguration(replicaId, clusterAddresses)))
configure()
}
notaryNode(0) {

View File

@ -58,7 +58,7 @@ class RaftNotaryCordform : CordformDefinition() {
fun notaryNode(index: Int, nodePort: Int, clusterPort: Int? = null, configure: CordformNode.() -> Unit) = node {
name(notaryNames[index])
val clusterAddresses = if (clusterPort != null) listOf(NetworkHostAndPort("localhost", clusterPort)) else emptyList()
notary(NotaryConfig(validating = true, raft = RaftConfig(NetworkHostAndPort("localhost", nodePort), clusterAddresses)))
notary(NotaryConfig(validating = true, serviceLegalName = clusterName, raft = RaftConfig(NetworkHostAndPort("localhost", nodePort), clusterAddresses)))
configure()
devMode(true)
}

View File

@ -546,6 +546,7 @@ class DriverDSLImpl(
val clusterAddresses = if (clusterAddress != null) listOf(clusterAddress) else emptyList()
val config = NotaryConfig(
validating = spec.validating,
serviceLegalName = spec.name,
raft = RaftConfig(nodeAddress = nodeAddress, clusterAddresses = clusterAddresses))
return config.toConfigMap()
}
@ -1130,5 +1131,3 @@ fun writeConfig(path: Path, filename: String, config: Config) {
private fun Config.toNodeOnly(): Config {
return if (hasPath("webAddress")) withoutPath("webAddress").withoutPath("useHTTPS") else this
}
private operator fun Config.plus(property: Pair<String, Any>) = withValue(property.first, ConfigValueFactory.fromAnyRef(property.second))

View File

@ -31,7 +31,8 @@ dependencies {
// Unit testing helpers.
compile "junit:junit:$junit_version"
compile 'org.hamcrest:hamcrest-library:1.3'
compile "com.nhaarman:mockito-kotlin:1.1.0"
compile 'com.nhaarman:mockito-kotlin:1.5.0'
compile "org.mockito:mockito-core:$mockito_version"
compile "org.assertj:assertj-core:$assertj_version"
// Guava: Google test library (collections test suite)

View File

@ -26,12 +26,8 @@ import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.serialization.amqp.AMQP_ENABLED
import org.mockito.Mockito
import org.mockito.internal.stubbing.answers.ThrowsException
import java.lang.reflect.Modifier
import java.nio.file.Files
import java.security.KeyPair
import java.util.*
import javax.security.auth.x500.X500Principal
@Suppress("unused")
@ -48,28 +44,6 @@ inline fun <reified T : Any> T.amqpSpecific(reason: String, function: () -> Unit
loggerFor<T>().info("Ignoring AMQP specific test, reason: $reason")
}
/**
* A method on a mock was called, but no behaviour was previously specified for that method.
* You can use [com.nhaarman.mockito_kotlin.doReturn] or similar to specify behaviour, see Mockito documentation for details.
*/
class UndefinedMockBehaviorException(message: String) : RuntimeException(message)
inline fun <reified T : Any> rigorousMock() = rigorousMock(T::class.java)
/**
* Create a Mockito mock that has [UndefinedMockBehaviorException] as the default behaviour of all abstract methods,
* and [org.mockito.invocation.InvocationOnMock.callRealMethod] as the default for all concrete methods.
* @param T the type to mock. Note if you want concrete methods of a Kotlin interface to be invoked,
* it won't work unless you mock a (trivial) abstract implementation of that interface instead.
*/
fun <T> rigorousMock(clazz: Class<T>): T = Mockito.mock(clazz) {
if (Modifier.isAbstract(it.method.modifiers)) {
// Use ThrowsException to hack the stack trace, and lazily so we can customise the message:
ThrowsException(UndefinedMockBehaviorException("Please specify what should happen when '${it.method}' is called, or don't call it. Args: ${Arrays.toString(it.arguments)}")).answer(it)
} else {
it.callRealMethod()
}
}
fun configureTestSSL(legalName: CordaX500Name): SSLConfiguration {
return object : SSLConfiguration {
override val certificatesDirectory = Files.createTempDirectory("certs")
@ -128,9 +102,6 @@ fun createDevNodeCaCertPath(
return Triple(rootCa, intermediateCa, nodeCa)
}
/** Application of [doAnswer] that gets a value from the given [map] using the arg at [argIndex] as key. */
fun doLookup(map: Map<*, *>, argIndex: Int = 0) = doAnswer { map[it.arguments[argIndex]] }
fun SSLConfiguration.useSslRpcOverrides(): Map<String, Any> {
return mapOf(
"rpcSettings.useSsl" to "true",

View File

@ -0,0 +1,119 @@
package net.corda.testing.internal
import com.nhaarman.mockito_kotlin.doAnswer
import net.corda.core.utilities.contextLogger
import org.mockito.Mockito
import org.mockito.exceptions.base.MockitoException
import org.mockito.internal.stubbing.answers.ThrowsException
import org.mockito.invocation.InvocationOnMock
import org.mockito.stubbing.Answer
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.util.*
import java.util.concurrent.ConcurrentHashMap
/**
* A method on a mock was called, but no behaviour was previously specified for that method.
* You can use [com.nhaarman.mockito_kotlin.doReturn] or similar to specify behaviour, see Mockito documentation for details.
*/
class UndefinedMockBehaviorException(message: String) : RuntimeException(message)
inline fun <reified T : Any> spectator() = spectator(T::class.java)
inline fun <reified T : Any> rigorousMock() = rigorousMock(T::class.java)
inline fun <reified T : Any> participant() = participant(T::class.java)
/**
* Create a Mockito mock where void methods do nothing, and any method of mockable return type will return another spectator,
* and multiple calls to such a method with equal args will return the same spectator.
* This is useful for an inconsequential service such as metrics or logging.
* Unlike plain old Mockito, methods that return primitives and unmockable types such as [String] remain unimplemented.
* Use sparingly, as any invalid behaviour caused by the implicitly-created spectators is likely to be difficult to diagnose.
* As in the other profiles, the exception is [toString] which has a simple reliable implementation for ease of debugging.
*/
fun <T> spectator(clazz: Class<out T>) = Mockito.mock(clazz, SpectatorDefaultAnswer())!!
/**
* Create a Mockito mock that inherits the implementations of all concrete methods from the given type.
* In particular this is convenient for mocking a Kotlin interface via a trivial abstract class.
* As in the other profiles, the exception is [toString] which has a simple reliable implementation for ease of debugging.
*/
fun <T> rigorousMock(clazz: Class<out T>) = Mockito.mock(clazz, RigorousMockDefaultAnswer)!!
/**
* Create a Mockito mock where all methods throw [UndefinedMockBehaviorException].
* Such mocks are useful when testing a grey box, for complete visibility and control over what methods it calls.
* As in the other profiles, the exception is [toString] which has a simple reliable implementation for ease of debugging.
*/
fun <T> participant(clazz: Class<out T>) = Mockito.mock(clazz, ParticipantDefaultAnswer)!!
private abstract class DefaultAnswer : Answer<Any?> {
internal abstract fun answerImpl(invocation: InvocationOnMock): Any?
override fun answer(invocation: InvocationOnMock): Any? {
val method = invocation.method
if (method.name == "toString" && method.parameterCount == 0) {
// Regular toString doesn't cache so neither do we:
val mock = invocation.mock
return "${mock.javaClass.simpleName}@${Integer.toHexString(mock.hashCode())}"
}
return answerImpl(invocation)
}
}
private class SpectatorDefaultAnswer : DefaultAnswer() {
private companion object {
private val log = contextLogger()
}
private class MethodInfo(invocation: InvocationOnMock) {
// FIXME LATER: The type resolution code probably doesn't cover all cases.
private val type = run {
val method = invocation.method
fun resolveType(context: Type, type: Type): Type {
context as? ParameterizedType ?: return type
val clazz = context.rawType as Class<*>
return context.actualTypeArguments[clazz.typeParameters.indexOf(resolveType(clazz.genericSuperclass, type))]
}
resolveType(invocation.mock.javaClass.genericSuperclass, method.genericReturnType) as? Class<*>
?: method.returnType!!
}
private fun newSpectator(invocation: InvocationOnMock) = spectator(type)!!.also { log.info("New spectator {} for: {}", it, invocation.arguments) }
private val spectators = try {
val first = newSpectator(invocation)
ConcurrentHashMap<InvocationOnMock, Any>().apply { put(invocation, first) }
} catch (e: MockitoException) {
null // A few types can't be mocked e.g. String.
}
internal fun spectator(invocation: InvocationOnMock) = spectators?.computeIfAbsent(invocation, ::newSpectator)
}
private val methods by lazy { ConcurrentHashMap<Method, MethodInfo>() }
override fun answerImpl(invocation: InvocationOnMock): Any? {
invocation.method.returnType.let {
it == Void.TYPE && return null
it.isPrimitive && return ParticipantDefaultAnswer.answerImpl(invocation)
}
return methods.computeIfAbsent(invocation.method) { MethodInfo(invocation) }.spectator(invocation)
?: ParticipantDefaultAnswer.answerImpl(invocation)
}
}
private object RigorousMockDefaultAnswer : DefaultAnswer() {
override fun answerImpl(invocation: InvocationOnMock): Any? {
return if (Modifier.isAbstract(invocation.method.modifiers)) ParticipantDefaultAnswer.answerImpl(invocation) else invocation.callRealMethod()
}
}
private object ParticipantDefaultAnswer : DefaultAnswer() {
override fun answerImpl(invocation: InvocationOnMock): Any? {
// Use ThrowsException to hack the stack trace, and lazily so we can customise the message:
return ThrowsException(UndefinedMockBehaviorException(
"Please specify what should happen when '${invocation.method}' is called, or don't call it. Args: ${Arrays.toString(invocation.arguments)}"))
.answer(invocation)
}
}
/** Application of [doAnswer] that gets a value from the given [map] using the arg at [argIndex] as key. */
fun doLookup(map: Map<*, *>, argIndex: Int = 0) = doAnswer { map[it.getArgument<Any?>(argIndex)] }

View File

@ -0,0 +1 @@
mock-maker-inline

View File

@ -0,0 +1,126 @@
package net.corda.testing.internal
import org.assertj.core.api.Assertions.catchThrowable
import org.hamcrest.Matchers.isA
import org.junit.Assert.assertThat
import org.junit.Test
import java.io.Closeable
import java.io.InputStream
import java.io.Serializable
import java.util.stream.Stream
import kotlin.test.*
private interface MyInterface {
fun abstractFun(): Int
fun kotlinDefaultFun() = 5
}
private abstract class MyAbstract : MyInterface
private open class MyImpl : MyInterface {
override fun abstractFun() = 4
open fun openFun() = 6
fun finalFun() = 7
override fun toString() = "8"
}
private interface MySpectator {
fun sideEffect()
fun noClearDefault(): Int
fun collaborator(arg: Int): MySpectator
}
class RigorousMockTest {
@Test
fun `toString has a reliable default answer in all cases`() {
Stream.of<(Class<out Any>) -> Any>(::spectator, ::rigorousMock, ::participant).forEach { profile ->
Stream.of(MyInterface::class, MyAbstract::class, MyImpl::class).forEach { type ->
val mock = profile(type.java)
assertEquals("${mock.javaClass.simpleName}@${Integer.toHexString(mock.hashCode())}", mock.toString())
}
}
}
@Test
fun `callRealMethod is preferred by rigorousMock`() {
rigorousMock<MyInterface>().let { m ->
assertSame<Any>(UndefinedMockBehaviorException::class.java, catchThrowable { m.abstractFun() }.javaClass)
assertSame<Any>(UndefinedMockBehaviorException::class.java, catchThrowable { m.kotlinDefaultFun() }.javaClass)
}
rigorousMock<MyAbstract>().let { m ->
assertSame<Any>(UndefinedMockBehaviorException::class.java, catchThrowable { m.abstractFun() }.javaClass)
assertEquals(5, m.kotlinDefaultFun())
}
rigorousMock<MyImpl>().let { m ->
assertEquals(4, m.abstractFun())
assertEquals(5, m.kotlinDefaultFun())
assertEquals(6, m.openFun())
assertEquals(7, m.finalFun())
}
}
@Test
fun `throw exception is preferred by participant`() {
participant<MyInterface>().let { m ->
assertSame<Any>(UndefinedMockBehaviorException::class.java, catchThrowable { m.abstractFun() }.javaClass)
assertSame<Any>(UndefinedMockBehaviorException::class.java, catchThrowable { m.kotlinDefaultFun() }.javaClass)
}
participant<MyAbstract>().let { m ->
assertSame<Any>(UndefinedMockBehaviorException::class.java, catchThrowable { m.abstractFun() }.javaClass)
assertSame<Any>(UndefinedMockBehaviorException::class.java, catchThrowable { m.kotlinDefaultFun() }.javaClass) // Broken in older Mockito.
}
participant<MyImpl>().let { m ->
assertSame<Any>(UndefinedMockBehaviorException::class.java, catchThrowable { m.abstractFun() }.javaClass)
assertSame<Any>(UndefinedMockBehaviorException::class.java, catchThrowable { m.kotlinDefaultFun() }.javaClass)
assertSame<Any>(UndefinedMockBehaviorException::class.java, catchThrowable { m.openFun() }.javaClass)
assertSame<Any>(UndefinedMockBehaviorException::class.java, catchThrowable { m.finalFun() }.javaClass)
}
}
@Test
fun `doing nothing is preferred by spectator`() {
val mock: MySpectator = spectator()
mock.sideEffect()
assertSame<Any>(UndefinedMockBehaviorException::class.java, catchThrowable { mock.noClearDefault() }.javaClass)
val collaborator = mock.collaborator(1)
assertNotSame(mock, collaborator)
assertSame(collaborator, mock.collaborator(1))
assertNotSame(collaborator, mock.collaborator(2))
collaborator.sideEffect()
assertSame<Any>(UndefinedMockBehaviorException::class.java, catchThrowable { collaborator.noClearDefault() }.javaClass)
}
private open class AB<out A, out B> {
val a: A get() = throw UnsupportedOperationException()
val b: B get() = throw UnsupportedOperationException()
}
private open class CD<out C, out D> : AB<D, C>()
private class CDImpl : CD<Runnable, String>()
@Test
fun `method return type resolution works`() {
val m = spectator<CDImpl>()
assertThat(m.b, isA(Runnable::class.java))
assertSame<Any>(UndefinedMockBehaviorException::class.java, catchThrowable { m.a }.javaClass) // Can't mock String.
}
private interface RS : Runnable, Serializable
private class TU<out T> where T : Runnable, T : Serializable {
fun t(): T = throw UnsupportedOperationException()
fun <U : Closeable> u(): U = throw UnsupportedOperationException()
}
@Test
fun `method return type erasure cases`() {
val m = spectator<TU<RS>>()
m.t().let { t: Any ->
assertFalse(t is RS)
assertTrue(t is Runnable)
assertFalse(t is Serializable) // Erasure picks the first bound.
}
m.u<InputStream>().let { u: Any ->
assertFalse(u is InputStream)
assertTrue(u is Closeable)
}
}
}

View File

@ -84,7 +84,6 @@ dependencies {
testCompile "junit:junit:$junit_version"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testCompile "org.assertj:assertj-core:${assertj_version}"
testCompile "org.mockito:mockito-core:$mockito_version"
}
jar {