diff --git a/.idea/compiler.xml b/.idea/compiler.xml index d37c3b63c3..85a974b2fe 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -60,6 +60,7 @@ + @@ -76,6 +77,8 @@ + + @@ -98,4 +101,4 @@ - + \ No newline at end of file diff --git a/client/rpc/build.gradle b/client/rpc/build.gradle index 7a6e7f6688..b18b563b0f 100644 --- a/client/rpc/build.gradle +++ b/client/rpc/build.gradle @@ -60,6 +60,7 @@ dependencies { testCompile project(':client:mock') // Smoke tests do NOT have any Node code on the classpath! + smokeTestCompile project(':smoke-test-utils') smokeTestCompile project(':finance') smokeTestCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" smokeTestCompile "org.apache.logging.log4j:log4j-core:$log4j_version" @@ -76,7 +77,6 @@ task integrationTest(type: Test) { task smokeTest(type: Test) { testClassesDir = sourceSets.smokeTest.output.classesDir classpath = sourceSets.smokeTest.runtimeClasspath - systemProperties['build.dir'] = buildDir } jar { diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 790cbd8ce6..50d6b90d9e 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -2,17 +2,11 @@ package net.corda.kotlin.rpc import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream -import java.io.FilterInputStream -import java.io.InputStream -import java.nio.file.Path -import java.nio.file.Paths -import java.time.Duration.ofSeconds -import java.util.Currency -import java.util.concurrent.atomic.AtomicInteger -import kotlin.test.* import net.corda.client.rpc.CordaRPCConnection import net.corda.client.rpc.notUsed -import net.corda.core.contracts.* +import net.corda.core.contracts.DOLLARS +import net.corda.core.contracts.POUNDS +import net.corda.core.contracts.SWISS_FRANCS import net.corda.core.crypto.SecureHash import net.corda.core.getOrThrow import net.corda.core.identity.Party @@ -20,27 +14,34 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.startFlow import net.corda.core.messaging.startTrackedFlow +import net.corda.core.seconds import net.corda.core.serialization.OpaqueBytes import net.corda.core.sizedInputStreamAndHash import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.loggerFor import net.corda.flows.CashIssueFlow import net.corda.nodeapi.User +import net.corda.smoketesting.NodeConfig +import net.corda.smoketesting.NodeProcess import org.apache.commons.io.output.NullOutputStream import org.junit.After import org.junit.Before import org.junit.Test +import java.io.FilterInputStream +import java.io.InputStream +import java.util.* +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals class StandaloneCordaRPClientTest { private companion object { val log = loggerFor() - val buildDir: Path = Paths.get(System.getProperty("build.dir")) - val nodesDir: Path = buildDir.resolve("nodes") val user = User("user1", "test", permissions = setOf("ALL")) - val factory = NodeProcess.Factory(nodesDir) val port = AtomicInteger(15000) const val attachmentSize = 2116 - const val timeout = 60L + val timeout = 60.seconds } private lateinit var notary: NodeProcess @@ -59,7 +60,7 @@ class StandaloneCordaRPClientTest { @Before fun setUp() { - notary = factory.create(notaryConfig) + notary = NodeProcess.Factory().create(notaryConfig) connection = notary.connect() rpcProxy = connection.proxy notaryIdentity = fetchNotaryIdentity() @@ -91,7 +92,7 @@ class StandaloneCordaRPClientTest { @Test fun `test starting flow`() { rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) - .returnValue.getOrThrow(ofSeconds(timeout)) + .returnValue.getOrThrow(timeout) } @Test @@ -104,7 +105,7 @@ class StandaloneCordaRPClientTest { log.info("Flow>> $msg") ++trackCount } - handle.returnValue.getOrThrow(ofSeconds(timeout)) + handle.returnValue.getOrThrow(timeout) assertNotEquals(0, trackCount) } @@ -128,7 +129,7 @@ class StandaloneCordaRPClientTest { // Now issue some cash rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) - .returnValue.getOrThrow(ofSeconds(timeout)) + .returnValue.getOrThrow(timeout) assertEquals(1, updateCount) } @@ -145,7 +146,7 @@ class StandaloneCordaRPClientTest { // Now issue some cash rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) - .returnValue.getOrThrow(ofSeconds(timeout)) + .returnValue.getOrThrow(timeout) assertNotEquals(0, updateCount) // Check that this cash exists in the vault diff --git a/core/src/main/kotlin/net/corda/core/Utils.kt b/core/src/main/kotlin/net/corda/core/Utils.kt index 1a74e0bb00..0e12cfdb04 100644 --- a/core/src/main/kotlin/net/corda/core/Utils.kt +++ b/core/src/main/kotlin/net/corda/core/Utils.kt @@ -32,7 +32,6 @@ import java.util.zip.Deflater import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream -import kotlin.collections.LinkedHashMap import kotlin.concurrent.withLock import kotlin.reflect.KProperty @@ -131,8 +130,8 @@ fun ListenableFuture.toObservable(): Observable { } /** Allows you to write code like: Paths.get("someDir") / "subdir" / "filename" but using the Paths API to avoid platform separator problems. */ -operator fun Path.div(other: String) = resolve(other) -operator fun String.div(other: String) = Paths.get(this) / other +operator fun Path.div(other: String): Path = resolve(other) +operator fun String.div(other: String): Path = Paths.get(this) / other fun Path.createDirectory(vararg attrs: FileAttribute<*>): Path = Files.createDirectory(this, *attrs) fun Path.createDirectories(vararg attrs: FileAttribute<*>): Path = Files.createDirectories(this, *attrs) diff --git a/core/src/main/kotlin/net/corda/core/node/services/CordaService.kt b/core/src/main/kotlin/net/corda/core/node/services/CordaService.kt index 61dab01549..658dafa9a2 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/CordaService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/CordaService.kt @@ -16,8 +16,7 @@ import kotlin.annotation.AnnotationTarget.CLASS * only loaded in nodes that declare the type in their advertisedServices. */ // TODO Handle the singleton serialisation of Corda services automatically, removing the need to implement SerializeAsToken -// TODO Currently all nodes which load the plugin will attempt to load the service even if it's not revelant to them. The -// underlying problem is that the entire CorDapp jar is used as a dependency, when in fact it's just the client-facing -// bit of the CorDapp that should be depended on (e.g. the initiating flows). +// TODO Perhaps this should be an interface or abstract class due to the need for it to implement SerializeAsToken and +// the need for the service type (which can be exposed by a simple getter) @Target(CLASS) annotation class CordaService diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 8888bb75ea..297304d9d9 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -138,7 +138,7 @@ class AttachmentSerializationTest { } private fun launchFlow(clientLogic: ClientLogic, rounds: Int) { - server.registerFlowFactory(ClientLogic::class.java, object : InitiatedFlowFactory { + server.internalRegisterFlowFactory(ClientLogic::class.java, object : InitiatedFlowFactory { override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): ServerLogic { return ServerLogic(otherParty) } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 1b267689e0..b6d35724d8 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -15,7 +15,11 @@ UNRELEASED * ``CordaPluginRegistry.servicePlugins`` is also no longer used, along with ``PluginServiceHub.registerFlowInitiator``. Instead annotate your initiated flows with ``@InitiatedBy``. This annotation takes a single parameter which is the initiating flow. This initiating flow further has to be annotated with ``@InitiatingFlow``. For any services you - may have, such as oracles, annotate them with ``@CordaService``. + may have, such as oracles, annotate them with ``@CordaService``. These annotations will be picked up automatically + when the node starts up. + + * Due to these changes, when unit testing flows make sure to use ``AbstractNode.registerInitiatedFlow`` so that the flows + are wired up. Likewise for services use ``AbstractNode.installCordaService``. * Related to ``InitiatingFlow``, the ``shareParentSessions`` boolean parameter of ``FlowLogic.subFlow`` has been removed. This was an unfortunate parameter that unnecessarily exposed the inner workings of flow sessions. Now, if diff --git a/docs/source/flow-testing.rst b/docs/source/flow-testing.rst index ba2f3178b1..acf3e86be2 100644 --- a/docs/source/flow-testing.rst +++ b/docs/source/flow-testing.rst @@ -80,4 +80,9 @@ valid) inside a ``database.transaction``. All node flows run within a database but any time we need to use the database directly from a unit test, you need to provide a database transaction as shown here. -And that's it: you can explore the documentation for the `MockNetwork API `_ here. +With regards to initiated flows (see :doc:`flow-state-machines` for information on initiated and initiating flows), the +full node automatically registers them by scanning the CorDapp jars. In a unit test environment this is not possible so +``MockNode`` has the ``registerInitiatedFlow`` method to manually register an initiated flow. + +And that's it: you can explore the documentation for the `MockNetwork API `_ +here. diff --git a/docs/source/oracles.rst b/docs/source/oracles.rst index d0086b980f..2fe049362e 100644 --- a/docs/source/oracles.rst +++ b/docs/source/oracles.rst @@ -275,3 +275,11 @@ Here's an example of it in action from ``FixingFlow.Fixer``. When overriding be careful when making the sub-class an anonymous or inner class (object declarations in Kotlin), because that kind of classes can access variables from the enclosing scope and cause serialization problems when checkpointed. + +Testing +------- + +When unit testing we make use of the ``MockNetwork`` which allows us to create ``MockNode``s, which are simplified nodes +suitable for tests. One feature we lose (and which is not suitable in unit testing anyway) is the node's ability to scan +and automatically install orcales it finds in the CorDapp jars. Instead when working with ``MockNode`` use the +``installCordaService`` method to manually install the oracle on the relevant node. \ No newline at end of file diff --git a/node/build.gradle b/node/build.gradle index f81f9b4044..0f26faf62c 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -25,6 +25,9 @@ configurations { integrationTestCompile.extendsFrom testCompile integrationTestRuntime.extendsFrom testRuntime + + smokeTestCompile.extendsFrom compile + smokeTestRuntime.extendsFrom runtime } sourceSets { @@ -38,6 +41,15 @@ sourceSets { srcDir file('src/integration-test/resources') } } + smokeTest { + kotlin { + // We must NOT have any Node code on the classpath, so do NOT + // include the test or integrationTest dependencies here. + compileClasspath += main.output + runtimeClasspath += main.output + srcDir file('src/smoke-test/kotlin') + } + } } // Use manual resource copying of log4j2.xml rather than source sets. @@ -46,12 +58,15 @@ processResources { from file("$rootDir/config/dev/log4j2.xml") } -processIntegrationTestResources { +processSmokeTestResources { // Build one of the demos so that we can test CorDapp scanning in CordappScanningTest. It doesn't matter which demo // we use, just make sure the test is updated accordingly. from(project(':samples:trader-demo').tasks.jar) { rename 'trader-demo-(.*)', 'trader-demo.jar' } + from(project(':node:capsule').tasks.buildCordaJAR) { + rename 'corda-(.*)', 'corda.jar' + } } // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in @@ -168,6 +183,11 @@ dependencies { // Integration test helpers integrationTestCompile "junit:junit:$junit_version" + + // Smoke tests do NOT have any Node code on the classpath! + smokeTestCompile project(':smoke-test-utils') + smokeTestCompile "org.assertj:assertj-core:${assertj_version}" + smokeTestCompile "junit:junit:$junit_version" } task integrationTest(type: Test) { @@ -175,6 +195,11 @@ task integrationTest(type: Test) { classpath = sourceSets.integrationTest.runtimeClasspath } +task smokeTest(type: Test) { + testClassesDir = sourceSets.smokeTest.output.classesDir + classpath = sourceSets.smokeTest.runtimeClasspath +} + jar { baseName 'corda-node' } diff --git a/node/src/main/kotlin/net/corda/node/driver/Driver.kt b/node/src/main/kotlin/net/corda/node/driver/Driver.kt index 2d72f47f90..d1f37bddfa 100644 --- a/node/src/main/kotlin/net/corda/node/driver/Driver.kt +++ b/node/src/main/kotlin/net/corda/node/driver/Driver.kt @@ -36,7 +36,6 @@ import okhttp3.Request import org.bouncycastle.asn1.x500.X500Name import org.slf4j.Logger import java.io.File -import java.io.File.pathSeparator import java.net.* import java.nio.file.Path import java.nio.file.Paths @@ -668,13 +667,19 @@ class DriverDSL( debugPort: Int?, overriddenSystemProperties: Map ): ListenableFuture { - return executorService.submit { + // Get the package of the caller of the driver and pass this to the node for CorDapp scanning + val callerPackage = Exception() + .stackTrace + .first { it.fileName != "Driver.kt" } + .let { Class.forName(it.className).`package`.name } + val processFuture = executorService.submit { // Write node.conf writeConfig(nodeConf.baseDirectory, "node.conf", config) val systemProperties = overriddenSystemProperties + mapOf( "name" to nodeConf.myLegalName, "visualvm.display.name" to "corda-${nodeConf.myLegalName}", + "net.corda.node.cordapp.scan.package" to callerPackage, "java.io.tmpdir" to System.getProperty("java.io.tmpdir") // Inherit from parent process ) // TODO Add this once we upgrade to quasar 0.7.8, this causes startup time to halve. @@ -685,8 +690,6 @@ class DriverDSL( "-javaagent:$quasarJarPath" val loggingLevel = if (debugPort == null) "INFO" else "DEBUG" - val pluginsDirectory = nodeConf.baseDirectory / "plugins" - ProcessUtilities.startJavaProcess( className = "net.corda.node.Corda", // cannot directly get class for this, so just use string arguments = listOf( @@ -694,14 +697,15 @@ class DriverDSL( "--logging-level=$loggingLevel", "--no-local-shell" ), - // Like the capsule, include the node's plugin directory - classpath = "${ProcessUtilities.defaultClassPath}$pathSeparator$pluginsDirectory/*", jdwpPort = debugPort, extraJvmArguments = extraJvmArguments, errorLogPath = nodeConf.baseDirectory / LOGS_DIRECTORY_NAME / "error.log", workingDirectory = nodeConf.baseDirectory ) - }.flatMap { process -> addressMustBeBoundFuture(executorService, nodeConf.p2pAddress, process).map { process } } + } + return processFuture.flatMap { + process -> addressMustBeBoundFuture(executorService, nodeConf.p2pAddress, process).map { process } + } } private fun startWebserver( diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index f35be3135b..e16cabe21c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -222,6 +222,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, serverThread, database, busyNodeLatch) + + smm.tokenizableServices.addAll(tokenizableServices) + if (serverThread is ExecutorService) { runOnStop += Runnable { // We wait here, even though any in-flight messages should have been drained away because the @@ -240,10 +243,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, startMessagingService(rpcOps) installCoreFlows() - val scanResult = scanCorDapps() + val scanResult = scanCordapps() if (scanResult != null) { - val cordappServices = installCordaServices(scanResult) - tokenizableServices.addAll(cordappServices) + installCordaServices(scanResult) registerInitiatedFlows(scanResult) rpcFlows = findRPCFlows(scanResult) } else { @@ -257,7 +259,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, runOnStop += Runnable { network.stop() } _networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured()) - smm.start(tokenizableServices) + smm.start() // Shut down the SMM so no Fibers are scheduled. runOnStop += Runnable { smm.stop(acceptableLiveFiberCountOnStop()) } scheduler.start() @@ -266,34 +268,37 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return this } - private fun installCordaServices(scanResult: ScanResult): List { - return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class).mapNotNull { - tryInstallCordaService(it) + private fun installCordaServices(scanResult: ScanResult) { + fun getServiceType(clazz: Class<*>): ServiceType? { + return try { + clazz.getField("type").get(null) as ServiceType + } catch (e: NoSuchFieldException) { + log.warn("${clazz.name} does not have a type field, optimistically proceeding with install.") + null + } } - } - private fun tryInstallCordaService(serviceClass: Class): T? { - /** TODO: This mechanism may get replaced by a different one, see comments on [CordaService]. */ - val typeField = try { - serviceClass.getField("type") - } catch (e: NoSuchFieldException) { - null - } - if (typeField == null) { - log.warn("${serviceClass.name} does not have a type field, optimistically proceeding with install.") - } else if (info.serviceIdentities(typeField.get(null) as ServiceType).isEmpty()) { - return null - } - return try { - installCordaService(serviceClass) - } catch (e: NoSuchMethodException) { - log.error("${serviceClass.name}, as a Corda service, must have a constructor with a single parameter " + - "of type ${PluginServiceHub::class.java.name}") - null - } catch (e: Exception) { - log.error("Unable to install Corda service ${serviceClass.name}", e) - null - } + return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class) + .filter { + val serviceType = getServiceType(it) + if (serviceType != null && info.serviceIdentities(serviceType).isEmpty()) { + log.debug { "Ignoring ${it.name} as a Corda service since $serviceType is not one of our " + + "advertised services" } + false + } else { + true + } + } + .forEach { + try { + installCordaService(it) + } catch (e: NoSuchMethodException) { + log.error("${it.name}, as a Corda service, must have a constructor with a single parameter " + + "of type ${PluginServiceHub::class.java.name}") + } catch (e: Exception) { + log.error("Unable to install Corda service ${it.name}", e) + } + } } /** @@ -305,6 +310,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val ctor = clazz.getDeclaredConstructor(PluginServiceHub::class.java).apply { isAccessible = true } val service = ctor.newInstance(services) cordappServices.putInstance(clazz, service) + smm.tokenizableServices += service log.info("Installed ${clazz.name} Corda service") return service } @@ -371,16 +377,16 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, "${InitiatingFlow::class.java.name} must be annotated on ${initiatingFlow.name} and not on a super-type" } val flowFactory = InitiatedFlowFactory.CorDapp(version, { ctor.newInstance(it) }) - val observable = registerFlowFactory(initiatingFlow, flowFactory, initiatedFlow, track) + val observable = internalRegisterFlowFactory(initiatingFlow, flowFactory, initiatedFlow, track) log.info("Registered ${initiatingFlow.name} to initiate ${initiatedFlow.name} (version $version)") return observable } @VisibleForTesting - fun > registerFlowFactory(initiatingFlowClass: Class>, - flowFactory: InitiatedFlowFactory, - initiatedFlowClass: Class, - track: Boolean): Observable { + fun > internalRegisterFlowFactory(initiatingFlowClass: Class>, + flowFactory: InitiatedFlowFactory, + initiatedFlowClass: Class, + track: Boolean): Observable { val observable = if (track) { smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass) } else { @@ -453,14 +459,15 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val tokenizableServices = mutableListOf(storage, network, vault, keyManagement, identity, platformClock, scheduler) makeAdvertisedServices(tokenizableServices) - return tokenizableServices } - private fun scanCorDapps(): ScanResult? { + private fun scanCordapps(): ScanResult? { val scanPackage = System.getProperty("net.corda.node.cordapp.scan.package") val paths = if (scanPackage != null) { - // This is purely for integration tests so that classes defined in the test can automatically be picked up + // Rather than looking in the plugins directory, figure out the classpath for the given package and scan that + // instead. This is used in tests where we avoid having to package stuff up in jars and then having to move + // them to the plugins directory for each node. check(configuration.devMode) { "Package scanning can only occur in dev mode" } val resource = scanPackage.replace('.', '/') javaClass.classLoader.getResources(resource) @@ -545,7 +552,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, private fun hasSSLCertificates(): Boolean { val (sslKeystore, keystore) = try { // This will throw IOException if key file not found or KeyStoreException if keystore password is incorrect. - Pair(KeyStoreUtilities.loadKeyStore(configuration.sslKeystore, configuration.keyStorePassword), KeyStoreUtilities.loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword)) + Pair( + KeyStoreUtilities.loadKeyStore(configuration.sslKeystore, configuration.keyStorePassword), + KeyStoreUtilities.loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword)) } catch (e: IOException) { return false } catch (e: KeyStoreException) { diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index db7d34b490..595ac8dc47 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -36,6 +36,7 @@ import rx.subjects.PublishSubject import java.util.* import java.util.concurrent.ConcurrentHashMap import javax.annotation.concurrent.ThreadSafe +import kotlin.collections.ArrayList /** * A StateMachineManager is responsible for coordination and persistence of multiple [FlowStateMachine] objects. @@ -145,8 +146,11 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, private val openSessions = ConcurrentHashMap() private val recentlyClosedSessions = ConcurrentHashMap() + internal val tokenizableServices = ArrayList() // Context for tokenized services in checkpoints - private lateinit var serializationContext: SerializeAsTokenContext + private val serializationContext by lazy { + SerializeAsTokenContext(tokenizableServices, quasarKryoPool, serviceHub) + } /** Returns a list of all state machines executing the given flow logic at the top level (subflows do not count) */ fun

, T> findStateMachines(flowClass: Class

): List>> { @@ -170,8 +174,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, */ val changes: Observable = mutex.content.changesPublisher.wrapWithDatabaseTransaction() - fun start(tokenizableServices: List) { - serializationContext = SerializeAsTokenContext(tokenizableServices, quasarKryoPool, serviceHub) + fun start() { restoreFibersFromCheckpoints() listenToLedgerTransactions() serviceHub.networkMapCache.mapServiceRegistered.then(executor) { resumeRestoredFibers() } @@ -348,7 +351,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, val initiatedFlowFactory = serviceHub.getFlowFactory(sessionInit.initiatingFlowClass) if (initiatedFlowFactory == null) { logger.warn("${sessionInit.initiatingFlowClass} has not been registered: $sessionInit") - sendSessionReject("${sessionInit.initiatingFlowClass.name} has not been registered with a service flow") + sendSessionReject("${sessionInit.initiatingFlowClass.name} has not been registered") return } diff --git a/node/src/integration-test/kotlin/net/corda/node/CordappScanningTest.kt b/node/src/smoke-test/kotlin/net/corda/node/CordappScanningTest.kt similarity index 62% rename from node/src/integration-test/kotlin/net/corda/node/CordappScanningTest.kt rename to node/src/smoke-test/kotlin/net/corda/node/CordappScanningTest.kt index ba98a0adbc..41f95a52e1 100644 --- a/node/src/integration-test/kotlin/net/corda/node/CordappScanningTest.kt +++ b/node/src/smoke-test/kotlin/net/corda/node/CordappScanningTest.kt @@ -18,40 +18,57 @@ import net.corda.core.utilities.unwrap import net.corda.node.driver.driver import net.corda.node.services.startFlowPermission import net.corda.nodeapi.User +import net.corda.smoketesting.NodeConfig +import net.corda.smoketesting.NodeProcess import org.assertj.core.api.Assertions.assertThat import org.junit.Test import java.nio.file.Paths +import java.util.concurrent.atomic.AtomicInteger class CordappScanningTest { + private companion object { + val user = User("user1", "test", permissions = setOf("ALL")) + val port = AtomicInteger(15100) + } + + private val factory = NodeProcess.Factory() + + private val aliceConfig = NodeConfig( + party = ALICE, + p2pPort = port.andIncrement, + rpcPort = port.andIncrement, + webPort = port.andIncrement, + extraServices = emptyList(), + users = listOf(user) + ) + @Test fun `CorDapp jar in plugins directory is scanned`() { - // If the CorDapp jar does't exist then run the integrationTestClasses gradle task + // If the CorDapp jar does't exist then run the smokeTestClasses gradle task val cordappJar = Paths.get(javaClass.getResource("/trader-demo.jar").toURI()) - driver { - val pluginsDir = (baseDirectory(ALICE.name) / "plugins").createDirectories() - cordappJar.copyToDirectory(pluginsDir) + val pluginsDir = (factory.baseDirectory(aliceConfig) / "plugins").createDirectories() + cordappJar.copyToDirectory(pluginsDir) - val user = User("u", "p", emptySet()) - val alice = startNode(ALICE.name, rpcUsers = listOf(user)).getOrThrow() - val rpc = alice.rpcClientToNode().start(user.username, user.password) - // If the CorDapp wasn't scanned then SellerFlow won't have been picked up as an RPC flow - assertThat(rpc.proxy.registeredFlows()).contains("net.corda.traderdemo.flow.SellerFlow") + factory.create(aliceConfig).use { + it.connect().use { + // If the CorDapp wasn't scanned then SellerFlow won't have been picked up as an RPC flow + assertThat(it.proxy.registeredFlows()).contains("net.corda.traderdemo.flow.SellerFlow") + } } } @Test fun `empty plugins directory`() { - driver { - val baseDirectory = baseDirectory(ALICE.name) - (baseDirectory / "plugins").createDirectories() - startNode(ALICE.name).getOrThrow() - } + (factory.baseDirectory(aliceConfig) / "plugins").createDirectories() + factory.create(aliceConfig).close() } @Test fun `sub-classed initiated flow pointing to the same initiating flow as its super-class`() { val user = User("u", "p", setOf(startFlowPermission())) - driver(systemProperties = mapOf("net.corda.node.cordapp.scan.package" to "net.corda.node")) { + // We don't use the factory for this test because we want the node to pick up the annotated flows below. The driver + // will do just that. + driver { val (alice, bob) = Futures.allAsList( startNode(ALICE.name, rpcUsers = listOf(user)), startNode(BOB.name)).getOrThrow() diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index 5f2cf0cb23..71754dd4d5 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -99,7 +99,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { smmHasRemovedAllFlows.countDown() } } - mockSMM.start(listOf(services, scheduler)) + mockSMM.start() services.smm = mockSMM scheduler.start() } @@ -124,7 +124,9 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { override fun isRelevant(ourKeys: Set): Boolean = true - override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? = ScheduledActivity(flowLogicRef, instant) + override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? { + return ScheduledActivity(flowLogicRef, instant) + } override val contract: Contract get() = throw UnsupportedOperationException() diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index cecc34d12e..54412d9e67 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -625,7 +625,7 @@ class FlowFrameworkTests { @Test fun `unsupported new flow version`() { - node2.registerFlowFactory( + node2.internalRegisterFlowFactory( UpgradedFlow::class.java, InitiatedFlowFactory.CorDapp(version = 1, factory = ::DoubleInlinedSubFlow), DoubleInlinedSubFlow::class.java, @@ -675,7 +675,7 @@ class FlowFrameworkTests { initiatingFlowClass: KClass>, noinline flowFactory: (Party) -> P): ListenableFuture

{ - val observable = registerFlowFactory(initiatingFlowClass.java, object : InitiatedFlowFactory

{ + val observable = internalRegisterFlowFactory(initiatingFlowClass.java, object : InitiatedFlowFactory

{ override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): P { return flowFactory(otherParty) } diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt index 9639272865..337311b1dc 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt @@ -33,11 +33,7 @@ class IRSDemoTest : IntegrationTestCategory { @Test fun `runs IRS demo`() { - driver( - useTestClock = true, - isDebug = true, - systemProperties = mapOf("net.corda.node.cordapp.scan.package" to "net.corda.irs")) - { + driver(useTestClock = true, isDebug = true) { val (controller, nodeA, nodeB) = Futures.allAsList( startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.Oracle.type))), startNode(DUMMY_BANK_A.name, rpcUsers = listOf(rpcUser)), diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt index a2ef62ec66..5c07e8852b 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt @@ -55,7 +55,8 @@ object NodeInterestRates { @Suspendable override fun call() { val request = receive(otherParty).unwrap { it } - send(otherParty, serviceHub.cordaService(Oracle::class.java).sign(request.ftx)) + val oracle = serviceHub.cordaService(Oracle::class.java) + send(otherParty, oracle.sign(request.ftx)) } } @@ -70,7 +71,8 @@ object NodeInterestRates { override fun call(): Unit { val request = receive(otherParty).unwrap { it } progressTracker.currentStep = RECEIVED - val answers = serviceHub.cordaService(Oracle::class.java).query(request.queries, request.deadline) + val oracle = serviceHub.cordaService(Oracle::class.java) + val answers = oracle.query(request.queries, request.deadline) progressTracker.currentStep = SENDING send(otherParty, answers) } diff --git a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt index 69283f7ce6..f5f7bf8b36 100644 --- a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt +++ b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt @@ -32,7 +32,7 @@ class SimmValuationTest : IntegrationTestCategory { @Test fun `runs SIMM valuation demo`() { - driver(isDebug = true, systemProperties = mapOf("net.corda.node.cordapp.scan.package" to "net.corda.vega")) { + driver(isDebug = true) { startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow() val (nodeA, nodeB) = Futures.allAsList(startNode(nodeALegalName), startNode(nodeBLegalName)).getOrThrow() val (nodeAApi, nodeBApi) = Futures.allAsList(startWebserver(nodeA), startWebserver(nodeB)) diff --git a/settings.gradle b/settings.gradle index c433b9b935..1e35747a78 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,6 +20,7 @@ include 'experimental:sandbox' include 'experimental:quasar-hook' include 'verifier' include 'test-utils' +include 'smoke-test-utils' include 'tools:explorer' include 'tools:explorer:capsule' include 'tools:demobench' diff --git a/smoke-test-utils/build.gradle b/smoke-test-utils/build.gradle new file mode 100644 index 0000000000..dcd94fae66 --- /dev/null +++ b/smoke-test-utils/build.gradle @@ -0,0 +1,8 @@ +apply plugin: 'kotlin' + +description 'Utilities needed for smoke tests in Corda' + +dependencies { + // Smoke tests do NOT have any Node code on the classpath! + compile project(':client:rpc') +} diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeConfig.kt b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt similarity index 79% rename from client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeConfig.kt rename to smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt index 75c4074be1..338c88d656 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeConfig.kt +++ b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt @@ -1,6 +1,10 @@ -package net.corda.kotlin.rpc +package net.corda.smoketesting -import com.typesafe.config.* +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory.empty +import com.typesafe.config.ConfigRenderOptions +import com.typesafe.config.ConfigValue +import com.typesafe.config.ConfigValueFactory import net.corda.core.crypto.commonName import net.corda.core.identity.Party import net.corda.nodeapi.User @@ -18,13 +22,13 @@ class NodeConfig( val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) } - val commonName: String = party.name.commonName + val commonName: String get() = party.name.commonName /* * The configuration object depends upon the networkMap, * which is mutable. */ - fun toFileConfig(): Config = ConfigFactory.empty() + fun toFileConfig(): Config = empty() .withValue("myLegalName", valueFor(party.name.toString())) .withValue("p2pAddress", addressValueFor(p2pPort)) .withValue("extraAdvertisedServiceIds", valueFor(extraServices)) @@ -42,7 +46,6 @@ class NodeConfig( private fun valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any) private fun addressValueFor(port: Int) = valueFor("localhost:$port") private inline fun optional(path: String, obj: T?, body: (Config, T) -> Config): Config { - val config = ConfigFactory.empty() - return if (obj == null) config else body(config, obj).atPath(path) + return if (obj == null) empty() else body(empty(), obj).atPath(path) } } diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeProcess.kt b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt similarity index 73% rename from client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeProcess.kt rename to smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index 3d81f9ad83..c939bd4758 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeProcess.kt +++ b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -1,16 +1,18 @@ -package net.corda.kotlin.rpc +package net.corda.smoketesting import com.google.common.net.HostAndPort import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCConnection +import net.corda.core.createDirectories +import net.corda.core.div import net.corda.core.utilities.loggerFor -import java.io.File -import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths +import java.time.Instant +import java.time.ZoneId.systemDefault +import java.time.format.DateTimeFormatter import java.util.concurrent.Executors import java.util.concurrent.TimeUnit.SECONDS -import kotlin.test.* class NodeProcess( val config: NodeConfig, @@ -21,9 +23,7 @@ class NodeProcess( private companion object { val log = loggerFor() val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java") - val corda = File(this::class.java.getResource("/corda.jar").toURI()) - val buildDir: Path = Paths.get(System.getProperty("build.dir")) - val capsuleDir: Path = buildDir.resolve("capsule") + val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(systemDefault()) } fun connect(): CordaRPCConnection { @@ -40,16 +40,20 @@ class NodeProcess( } log.info("Deleting Artemis directories, because they're large!") - nodeDir.resolve("artemis").toFile().deleteRecursively() + (nodeDir / "artemis").toFile().deleteRecursively() } - class Factory(val nodesDir: Path) { + class Factory(val buildDirectory: Path = Paths.get("build"), + val cordaJar: Path = Paths.get(this::class.java.getResource("/corda.jar").toURI())) { + val nodesDirectory = buildDirectory / formatter.format(Instant.now()) init { - assertTrue(nodesDir.toFile().forceDirectory(), "Directory '$nodesDir' does not exist") + nodesDirectory.createDirectories() } + fun baseDirectory(config: NodeConfig): Path = nodesDirectory / config.commonName + fun create(config: NodeConfig): NodeProcess { - val nodeDir = Files.createTempDirectory(nodesDir, config.commonName) + val nodeDir = baseDirectory(config).createDirectories() log.info("Node directory: {}", nodeDir) val confFile = nodeDir.resolve("node.conf").toFile() @@ -78,7 +82,7 @@ class NodeProcess( }, 5, 1, SECONDS) val setupOK = setupExecutor.awaitTermination(120, SECONDS) - assertTrue(setupOK && process.isAlive, "Failed to create RPC connection") + check(setupOK && process.isAlive) { "Failed to create RPC connection" } } catch (e: Exception) { process.destroyForcibly() throw e @@ -91,17 +95,14 @@ class NodeProcess( private fun startNode(nodeDir: Path): Process { val builder = ProcessBuilder() - .command(javaPath.toString(), "-jar", corda.path) + .command(javaPath.toString(), "-jar", cordaJar.toString()) .directory(nodeDir.toFile()) builder.environment().putAll(mapOf( - "CAPSULE_CACHE_DIR" to capsuleDir.toString() + "CAPSULE_CACHE_DIR" to (buildDirectory / "capsule").toString() )) return builder.start() } } } - -private fun File.forceDirectory(): Boolean = this.isDirectory || this.mkdirs() -