mirror of
https://github.com/corda/corda.git
synced 2025-03-19 18:45:28 +00:00
Merge remote-tracking branch 'open-source/master' into thomas-merge-3d50e73271cb3bbd95822575349995c9771729dd
This commit is contained in:
commit
23a0a66128
@ -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'
|
||||
|
@ -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
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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] } }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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)] }
|
@ -0,0 +1 @@
|
||||
mock-maker-inline
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user