Fixes relating to testing flows and services.

Fixed issue where Corda services installed in unit tests were not being marked as serialise as singleton. Also the driver now automatically picks up the scanning annotations. This required moving the NodeFactory used in smoke tests into a separate module.
This commit is contained in:
Shams Asari 2017-06-02 15:47:20 +01:00
parent 08cbcac40c
commit afa3efb308
23 changed files with 220 additions and 130 deletions

3
.idea/compiler.xml generated
View File

@ -60,6 +60,7 @@
<module name="node-schemas_test" target="1.8" /> <module name="node-schemas_test" target="1.8" />
<module name="node_integrationTest" target="1.8" /> <module name="node_integrationTest" target="1.8" />
<module name="node_main" target="1.8" /> <module name="node_main" target="1.8" />
<module name="node_smokeTest" target="1.8" />
<module name="node_test" target="1.8" /> <module name="node_test" target="1.8" />
<module name="notary-demo_main" target="1.8" /> <module name="notary-demo_main" target="1.8" />
<module name="notary-demo_test" target="1.8" /> <module name="notary-demo_test" target="1.8" />
@ -76,6 +77,8 @@
<module name="simm-valuation-demo_integrationTest" target="1.8" /> <module name="simm-valuation-demo_integrationTest" target="1.8" />
<module name="simm-valuation-demo_main" target="1.8" /> <module name="simm-valuation-demo_main" target="1.8" />
<module name="simm-valuation-demo_test" target="1.8" /> <module name="simm-valuation-demo_test" target="1.8" />
<module name="smoke-test-utils_main" target="1.8" />
<module name="smoke-test-utils_test" target="1.8" />
<module name="test-utils_main" target="1.8" /> <module name="test-utils_main" target="1.8" />
<module name="test-utils_test" target="1.8" /> <module name="test-utils_test" target="1.8" />
<module name="tools_main" target="1.8" /> <module name="tools_main" target="1.8" />

View File

@ -60,6 +60,7 @@ dependencies {
testCompile project(':client:mock') testCompile project(':client:mock')
// Smoke tests do NOT have any Node code on the classpath! // Smoke tests do NOT have any Node code on the classpath!
smokeTestCompile project(':smoke-test-utils')
smokeTestCompile project(':finance') smokeTestCompile project(':finance')
smokeTestCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" smokeTestCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
smokeTestCompile "org.apache.logging.log4j:log4j-core:$log4j_version" smokeTestCompile "org.apache.logging.log4j:log4j-core:$log4j_version"
@ -76,7 +77,6 @@ task integrationTest(type: Test) {
task smokeTest(type: Test) { task smokeTest(type: Test) {
testClassesDir = sourceSets.smokeTest.output.classesDir testClassesDir = sourceSets.smokeTest.output.classesDir
classpath = sourceSets.smokeTest.runtimeClasspath classpath = sourceSets.smokeTest.runtimeClasspath
systemProperties['build.dir'] = buildDir
} }
jar { jar {

View File

@ -2,17 +2,11 @@ package net.corda.kotlin.rpc
import com.google.common.hash.Hashing import com.google.common.hash.Hashing
import com.google.common.hash.HashingInputStream 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.CordaRPCConnection
import net.corda.client.rpc.notUsed 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.crypto.SecureHash
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.identity.Party 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.StateMachineUpdate
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.messaging.startTrackedFlow import net.corda.core.messaging.startTrackedFlow
import net.corda.core.seconds
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.sizedInputStreamAndHash import net.corda.core.sizedInputStreamAndHash
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.flows.CashIssueFlow import net.corda.flows.CashIssueFlow
import net.corda.nodeapi.User import net.corda.nodeapi.User
import net.corda.smoketesting.NodeConfig
import net.corda.smoketesting.NodeProcess
import org.apache.commons.io.output.NullOutputStream import org.apache.commons.io.output.NullOutputStream
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test 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 { class StandaloneCordaRPClientTest {
private companion object { private companion object {
val log = loggerFor<StandaloneCordaRPClientTest>() val log = loggerFor<StandaloneCordaRPClientTest>()
val buildDir: Path = Paths.get(System.getProperty("build.dir"))
val nodesDir: Path = buildDir.resolve("nodes")
val user = User("user1", "test", permissions = setOf("ALL")) val user = User("user1", "test", permissions = setOf("ALL"))
val factory = NodeProcess.Factory(nodesDir)
val port = AtomicInteger(15000) val port = AtomicInteger(15000)
const val attachmentSize = 2116 const val attachmentSize = 2116
const val timeout = 60L val timeout = 60.seconds
} }
private lateinit var notary: NodeProcess private lateinit var notary: NodeProcess
@ -59,7 +60,7 @@ class StandaloneCordaRPClientTest {
@Before @Before
fun setUp() { fun setUp() {
notary = factory.create(notaryConfig) notary = NodeProcess.Factory().create(notaryConfig)
connection = notary.connect() connection = notary.connect()
rpcProxy = connection.proxy rpcProxy = connection.proxy
notaryIdentity = fetchNotaryIdentity() notaryIdentity = fetchNotaryIdentity()
@ -91,7 +92,7 @@ class StandaloneCordaRPClientTest {
@Test @Test
fun `test starting flow`() { fun `test starting flow`() {
rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity)
.returnValue.getOrThrow(ofSeconds(timeout)) .returnValue.getOrThrow(timeout)
} }
@Test @Test
@ -104,7 +105,7 @@ class StandaloneCordaRPClientTest {
log.info("Flow>> $msg") log.info("Flow>> $msg")
++trackCount ++trackCount
} }
handle.returnValue.getOrThrow(ofSeconds(timeout)) handle.returnValue.getOrThrow(timeout)
assertNotEquals(0, trackCount) assertNotEquals(0, trackCount)
} }
@ -128,7 +129,7 @@ class StandaloneCordaRPClientTest {
// Now issue some cash // Now issue some cash
rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity)
.returnValue.getOrThrow(ofSeconds(timeout)) .returnValue.getOrThrow(timeout)
assertEquals(1, updateCount) assertEquals(1, updateCount)
} }
@ -145,7 +146,7 @@ class StandaloneCordaRPClientTest {
// Now issue some cash // Now issue some cash
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity)
.returnValue.getOrThrow(ofSeconds(timeout)) .returnValue.getOrThrow(timeout)
assertNotEquals(0, updateCount) assertNotEquals(0, updateCount)
// Check that this cash exists in the vault // Check that this cash exists in the vault

View File

@ -32,7 +32,6 @@ import java.util.zip.Deflater
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
import kotlin.collections.LinkedHashMap
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@ -131,8 +130,8 @@ fun <A> ListenableFuture<out A>.toObservable(): Observable<A> {
} }
/** Allows you to write code like: Paths.get("someDir") / "subdir" / "filename" but using the Paths API to avoid platform separator problems. */ /** 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 Path.div(other: String): Path = resolve(other)
operator fun String.div(other: String) = Paths.get(this) / 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.createDirectory(vararg attrs: FileAttribute<*>): Path = Files.createDirectory(this, *attrs)
fun Path.createDirectories(vararg attrs: FileAttribute<*>): Path = Files.createDirectories(this, *attrs) fun Path.createDirectories(vararg attrs: FileAttribute<*>): Path = Files.createDirectories(this, *attrs)

View File

@ -16,8 +16,7 @@ import kotlin.annotation.AnnotationTarget.CLASS
* only loaded in nodes that declare the type in their advertisedServices. * 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 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 // TODO Perhaps this should be an interface or abstract class due to the need for it to implement SerializeAsToken and
// underlying problem is that the entire CorDapp jar is used as a dependency, when in fact it's just the client-facing // the need for the service type (which can be exposed by a simple getter)
// bit of the CorDapp that should be depended on (e.g. the initiating flows).
@Target(CLASS) @Target(CLASS)
annotation class CordaService annotation class CordaService

View File

@ -138,7 +138,7 @@ class AttachmentSerializationTest {
} }
private fun launchFlow(clientLogic: ClientLogic, rounds: Int) { private fun launchFlow(clientLogic: ClientLogic, rounds: Int) {
server.registerFlowFactory(ClientLogic::class.java, object : InitiatedFlowFactory<ServerLogic> { server.internalRegisterFlowFactory(ClientLogic::class.java, object : InitiatedFlowFactory<ServerLogic> {
override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): ServerLogic { override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): ServerLogic {
return ServerLogic(otherParty) return ServerLogic(otherParty)
} }

View File

@ -15,7 +15,11 @@ UNRELEASED
* ``CordaPluginRegistry.servicePlugins`` is also no longer used, along with ``PluginServiceHub.registerFlowInitiator``. * ``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 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 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 * 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 removed. This was an unfortunate parameter that unnecessarily exposed the inner workings of flow sessions. Now, if

View File

@ -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 but any time we need to use the database directly from a unit test, you need to provide a database transaction as shown
here. here.
And that's it: you can explore the documentation for the `MockNetwork API <api/kotlin/corda/net.corda.testing.node/-mock-network/index.html>`_ 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 <api/kotlin/corda/net.corda.testing.node/-mock-network/index.html>`_
here.

View File

@ -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), 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 because that kind of classes can access variables from the enclosing scope and cause serialization problems when
checkpointed. 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.

View File

@ -25,6 +25,9 @@ configurations {
integrationTestCompile.extendsFrom testCompile integrationTestCompile.extendsFrom testCompile
integrationTestRuntime.extendsFrom testRuntime integrationTestRuntime.extendsFrom testRuntime
smokeTestCompile.extendsFrom compile
smokeTestRuntime.extendsFrom runtime
} }
sourceSets { sourceSets {
@ -38,6 +41,15 @@ sourceSets {
srcDir file('src/integration-test/resources') 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. // Use manual resource copying of log4j2.xml rather than source sets.
@ -46,12 +58,15 @@ processResources {
from file("$rootDir/config/dev/log4j2.xml") 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 // 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. // we use, just make sure the test is updated accordingly.
from(project(':samples:trader-demo').tasks.jar) { from(project(':samples:trader-demo').tasks.jar) {
rename 'trader-demo-(.*)', 'trader-demo.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 // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
@ -168,6 +183,11 @@ dependencies {
// Integration test helpers // Integration test helpers
integrationTestCompile "junit:junit:$junit_version" 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) { task integrationTest(type: Test) {
@ -175,6 +195,11 @@ task integrationTest(type: Test) {
classpath = sourceSets.integrationTest.runtimeClasspath classpath = sourceSets.integrationTest.runtimeClasspath
} }
task smokeTest(type: Test) {
testClassesDir = sourceSets.smokeTest.output.classesDir
classpath = sourceSets.smokeTest.runtimeClasspath
}
jar { jar {
baseName 'corda-node' baseName 'corda-node'
} }

View File

@ -36,7 +36,6 @@ import okhttp3.Request
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.slf4j.Logger import org.slf4j.Logger
import java.io.File import java.io.File
import java.io.File.pathSeparator
import java.net.* import java.net.*
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
@ -668,13 +667,19 @@ class DriverDSL(
debugPort: Int?, debugPort: Int?,
overriddenSystemProperties: Map<String, String> overriddenSystemProperties: Map<String, String>
): ListenableFuture<Process> { ): ListenableFuture<Process> {
return executorService.submit<Process> { // 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<Process> {
// Write node.conf // Write node.conf
writeConfig(nodeConf.baseDirectory, "node.conf", config) writeConfig(nodeConf.baseDirectory, "node.conf", config)
val systemProperties = overriddenSystemProperties + mapOf( val systemProperties = overriddenSystemProperties + mapOf(
"name" to nodeConf.myLegalName, "name" to nodeConf.myLegalName,
"visualvm.display.name" to "corda-${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 "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. // 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" "-javaagent:$quasarJarPath"
val loggingLevel = if (debugPort == null) "INFO" else "DEBUG" val loggingLevel = if (debugPort == null) "INFO" else "DEBUG"
val pluginsDirectory = nodeConf.baseDirectory / "plugins"
ProcessUtilities.startJavaProcess( ProcessUtilities.startJavaProcess(
className = "net.corda.node.Corda", // cannot directly get class for this, so just use string className = "net.corda.node.Corda", // cannot directly get class for this, so just use string
arguments = listOf( arguments = listOf(
@ -694,14 +697,15 @@ class DriverDSL(
"--logging-level=$loggingLevel", "--logging-level=$loggingLevel",
"--no-local-shell" "--no-local-shell"
), ),
// Like the capsule, include the node's plugin directory
classpath = "${ProcessUtilities.defaultClassPath}$pathSeparator$pluginsDirectory/*",
jdwpPort = debugPort, jdwpPort = debugPort,
extraJvmArguments = extraJvmArguments, extraJvmArguments = extraJvmArguments,
errorLogPath = nodeConf.baseDirectory / LOGS_DIRECTORY_NAME / "error.log", errorLogPath = nodeConf.baseDirectory / LOGS_DIRECTORY_NAME / "error.log",
workingDirectory = nodeConf.baseDirectory 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( private fun startWebserver(

View File

@ -222,6 +222,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
serverThread, serverThread,
database, database,
busyNodeLatch) busyNodeLatch)
smm.tokenizableServices.addAll(tokenizableServices)
if (serverThread is ExecutorService) { if (serverThread is ExecutorService) {
runOnStop += Runnable { runOnStop += Runnable {
// We wait here, even though any in-flight messages should have been drained away because the // 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) startMessagingService(rpcOps)
installCoreFlows() installCoreFlows()
val scanResult = scanCorDapps() val scanResult = scanCordapps()
if (scanResult != null) { if (scanResult != null) {
val cordappServices = installCordaServices(scanResult) installCordaServices(scanResult)
tokenizableServices.addAll(cordappServices)
registerInitiatedFlows(scanResult) registerInitiatedFlows(scanResult)
rpcFlows = findRPCFlows(scanResult) rpcFlows = findRPCFlows(scanResult)
} else { } else {
@ -257,7 +259,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
runOnStop += Runnable { network.stop() } runOnStop += Runnable { network.stop() }
_networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured()) _networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured())
smm.start(tokenizableServices) smm.start()
// Shut down the SMM so no Fibers are scheduled. // Shut down the SMM so no Fibers are scheduled.
runOnStop += Runnable { smm.stop(acceptableLiveFiberCountOnStop()) } runOnStop += Runnable { smm.stop(acceptableLiveFiberCountOnStop()) }
scheduler.start() scheduler.start()
@ -266,33 +268,36 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
return this return this
} }
private fun installCordaServices(scanResult: ScanResult): List<SerializeAsToken> { private fun installCordaServices(scanResult: ScanResult) {
return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class).mapNotNull { fun getServiceType(clazz: Class<*>): ServiceType? {
tryInstallCordaService(it) 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 <T : SerializeAsToken> tryInstallCordaService(serviceClass: Class<T>): T? { return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class)
/** TODO: This mechanism may get replaced by a different one, see comments on [CordaService]. */ .filter {
val typeField = try { val serviceType = getServiceType(it)
serviceClass.getField("type") if (serviceType != null && info.serviceIdentities(serviceType).isEmpty()) {
} catch (e: NoSuchFieldException) { log.debug { "Ignoring ${it.name} as a Corda service since $serviceType is not one of our " +
null "advertised services" }
false
} else {
true
} }
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 { .forEach {
installCordaService(serviceClass) try {
installCordaService(it)
} catch (e: NoSuchMethodException) { } catch (e: NoSuchMethodException) {
log.error("${serviceClass.name}, as a Corda service, must have a constructor with a single parameter " + log.error("${it.name}, as a Corda service, must have a constructor with a single parameter " +
"of type ${PluginServiceHub::class.java.name}") "of type ${PluginServiceHub::class.java.name}")
null
} catch (e: Exception) { } catch (e: Exception) {
log.error("Unable to install Corda service ${serviceClass.name}", e) log.error("Unable to install Corda service ${it.name}", e)
null }
} }
} }
@ -305,6 +310,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
val ctor = clazz.getDeclaredConstructor(PluginServiceHub::class.java).apply { isAccessible = true } val ctor = clazz.getDeclaredConstructor(PluginServiceHub::class.java).apply { isAccessible = true }
val service = ctor.newInstance(services) val service = ctor.newInstance(services)
cordappServices.putInstance(clazz, service) cordappServices.putInstance(clazz, service)
smm.tokenizableServices += service
log.info("Installed ${clazz.name} Corda service") log.info("Installed ${clazz.name} Corda service")
return service return service
} }
@ -371,13 +377,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
"${InitiatingFlow::class.java.name} must be annotated on ${initiatingFlow.name} and not on a super-type" "${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 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)") log.info("Registered ${initiatingFlow.name} to initiate ${initiatedFlow.name} (version $version)")
return observable return observable
} }
@VisibleForTesting @VisibleForTesting
fun <F : FlowLogic<*>> registerFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>, fun <F : FlowLogic<*>> internalRegisterFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>,
flowFactory: InitiatedFlowFactory<F>, flowFactory: InitiatedFlowFactory<F>,
initiatedFlowClass: Class<F>, initiatedFlowClass: Class<F>,
track: Boolean): Observable<F> { track: Boolean): Observable<F> {
@ -453,14 +459,15 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
val tokenizableServices = mutableListOf(storage, network, vault, keyManagement, identity, platformClock, scheduler) val tokenizableServices = mutableListOf(storage, network, vault, keyManagement, identity, platformClock, scheduler)
makeAdvertisedServices(tokenizableServices) makeAdvertisedServices(tokenizableServices)
return tokenizableServices return tokenizableServices
} }
private fun scanCorDapps(): ScanResult? { private fun scanCordapps(): ScanResult? {
val scanPackage = System.getProperty("net.corda.node.cordapp.scan.package") val scanPackage = System.getProperty("net.corda.node.cordapp.scan.package")
val paths = if (scanPackage != null) { 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" } check(configuration.devMode) { "Package scanning can only occur in dev mode" }
val resource = scanPackage.replace('.', '/') val resource = scanPackage.replace('.', '/')
javaClass.classLoader.getResources(resource) javaClass.classLoader.getResources(resource)
@ -545,7 +552,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
private fun hasSSLCertificates(): Boolean { private fun hasSSLCertificates(): Boolean {
val (sslKeystore, keystore) = try { val (sslKeystore, keystore) = try {
// This will throw IOException if key file not found or KeyStoreException if keystore password is incorrect. // 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) { } catch (e: IOException) {
return false return false
} catch (e: KeyStoreException) { } catch (e: KeyStoreException) {

View File

@ -36,6 +36,7 @@ import rx.subjects.PublishSubject
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
import kotlin.collections.ArrayList
/** /**
* A StateMachineManager is responsible for coordination and persistence of multiple [FlowStateMachine] objects. * 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<Long, FlowSession>() private val openSessions = ConcurrentHashMap<Long, FlowSession>()
private val recentlyClosedSessions = ConcurrentHashMap<Long, Party>() private val recentlyClosedSessions = ConcurrentHashMap<Long, Party>()
internal val tokenizableServices = ArrayList<Any>()
// Context for tokenized services in checkpoints // 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) */ /** Returns a list of all state machines executing the given flow logic at the top level (subflows do not count) */
fun <P : FlowLogic<T>, T> findStateMachines(flowClass: Class<P>): List<Pair<P, ListenableFuture<T>>> { fun <P : FlowLogic<T>, T> findStateMachines(flowClass: Class<P>): List<Pair<P, ListenableFuture<T>>> {
@ -170,8 +174,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
*/ */
val changes: Observable<Change> = mutex.content.changesPublisher.wrapWithDatabaseTransaction() val changes: Observable<Change> = mutex.content.changesPublisher.wrapWithDatabaseTransaction()
fun start(tokenizableServices: List<Any>) { fun start() {
serializationContext = SerializeAsTokenContext(tokenizableServices, quasarKryoPool, serviceHub)
restoreFibersFromCheckpoints() restoreFibersFromCheckpoints()
listenToLedgerTransactions() listenToLedgerTransactions()
serviceHub.networkMapCache.mapServiceRegistered.then(executor) { resumeRestoredFibers() } serviceHub.networkMapCache.mapServiceRegistered.then(executor) { resumeRestoredFibers() }
@ -348,7 +351,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
val initiatedFlowFactory = serviceHub.getFlowFactory(sessionInit.initiatingFlowClass) val initiatedFlowFactory = serviceHub.getFlowFactory(sessionInit.initiatingFlowClass)
if (initiatedFlowFactory == null) { if (initiatedFlowFactory == null) {
logger.warn("${sessionInit.initiatingFlowClass} has not been registered: $sessionInit") 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 return
} }

View File

@ -18,40 +18,57 @@ import net.corda.core.utilities.unwrap
import net.corda.node.driver.driver import net.corda.node.driver.driver
import net.corda.node.services.startFlowPermission import net.corda.node.services.startFlowPermission
import net.corda.nodeapi.User import net.corda.nodeapi.User
import net.corda.smoketesting.NodeConfig
import net.corda.smoketesting.NodeProcess
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Test import org.junit.Test
import java.nio.file.Paths import java.nio.file.Paths
import java.util.concurrent.atomic.AtomicInteger
class CordappScanningTest { 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 @Test
fun `CorDapp jar in plugins directory is scanned`() { 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()) val cordappJar = Paths.get(javaClass.getResource("/trader-demo.jar").toURI())
driver { val pluginsDir = (factory.baseDirectory(aliceConfig) / "plugins").createDirectories()
val pluginsDir = (baseDirectory(ALICE.name) / "plugins").createDirectories()
cordappJar.copyToDirectory(pluginsDir) cordappJar.copyToDirectory(pluginsDir)
val user = User("u", "p", emptySet()) factory.create(aliceConfig).use {
val alice = startNode(ALICE.name, rpcUsers = listOf(user)).getOrThrow() it.connect().use {
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 // 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") assertThat(it.proxy.registeredFlows()).contains("net.corda.traderdemo.flow.SellerFlow")
}
} }
} }
@Test @Test
fun `empty plugins directory`() { fun `empty plugins directory`() {
driver { (factory.baseDirectory(aliceConfig) / "plugins").createDirectories()
val baseDirectory = baseDirectory(ALICE.name) factory.create(aliceConfig).close()
(baseDirectory / "plugins").createDirectories()
startNode(ALICE.name).getOrThrow()
}
} }
@Test @Test
fun `sub-classed initiated flow pointing to the same initiating flow as its super-class`() { fun `sub-classed initiated flow pointing to the same initiating flow as its super-class`() {
val user = User("u", "p", setOf(startFlowPermission<ReceiveFlow>())) val user = User("u", "p", setOf(startFlowPermission<ReceiveFlow>()))
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( val (alice, bob) = Futures.allAsList(
startNode(ALICE.name, rpcUsers = listOf(user)), startNode(ALICE.name, rpcUsers = listOf(user)),
startNode(BOB.name)).getOrThrow() startNode(BOB.name)).getOrThrow()

View File

@ -99,7 +99,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
smmHasRemovedAllFlows.countDown() smmHasRemovedAllFlows.countDown()
} }
} }
mockSMM.start(listOf(services, scheduler)) mockSMM.start()
services.smm = mockSMM services.smm = mockSMM
scheduler.start() scheduler.start()
} }
@ -124,7 +124,9 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean = true override fun isRelevant(ourKeys: Set<PublicKey>): 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 override val contract: Contract
get() = throw UnsupportedOperationException() get() = throw UnsupportedOperationException()

View File

@ -625,7 +625,7 @@ class FlowFrameworkTests {
@Test @Test
fun `unsupported new flow version`() { fun `unsupported new flow version`() {
node2.registerFlowFactory( node2.internalRegisterFlowFactory(
UpgradedFlow::class.java, UpgradedFlow::class.java,
InitiatedFlowFactory.CorDapp(version = 1, factory = ::DoubleInlinedSubFlow), InitiatedFlowFactory.CorDapp(version = 1, factory = ::DoubleInlinedSubFlow),
DoubleInlinedSubFlow::class.java, DoubleInlinedSubFlow::class.java,
@ -675,7 +675,7 @@ class FlowFrameworkTests {
initiatingFlowClass: KClass<out FlowLogic<*>>, initiatingFlowClass: KClass<out FlowLogic<*>>,
noinline flowFactory: (Party) -> P): ListenableFuture<P> noinline flowFactory: (Party) -> P): ListenableFuture<P>
{ {
val observable = registerFlowFactory(initiatingFlowClass.java, object : InitiatedFlowFactory<P> { val observable = internalRegisterFlowFactory(initiatingFlowClass.java, object : InitiatedFlowFactory<P> {
override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): P { override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): P {
return flowFactory(otherParty) return flowFactory(otherParty)
} }

View File

@ -33,11 +33,7 @@ class IRSDemoTest : IntegrationTestCategory {
@Test @Test
fun `runs IRS demo`() { fun `runs IRS demo`() {
driver( driver(useTestClock = true, isDebug = true) {
useTestClock = true,
isDebug = true,
systemProperties = mapOf("net.corda.node.cordapp.scan.package" to "net.corda.irs"))
{
val (controller, nodeA, nodeB) = Futures.allAsList( val (controller, nodeA, nodeB) = Futures.allAsList(
startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.Oracle.type))), startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.Oracle.type))),
startNode(DUMMY_BANK_A.name, rpcUsers = listOf(rpcUser)), startNode(DUMMY_BANK_A.name, rpcUsers = listOf(rpcUser)),

View File

@ -55,7 +55,8 @@ object NodeInterestRates {
@Suspendable @Suspendable
override fun call() { override fun call() {
val request = receive<RatesFixFlow.SignRequest>(otherParty).unwrap { it } val request = receive<RatesFixFlow.SignRequest>(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 { override fun call(): Unit {
val request = receive<RatesFixFlow.QueryRequest>(otherParty).unwrap { it } val request = receive<RatesFixFlow.QueryRequest>(otherParty).unwrap { it }
progressTracker.currentStep = RECEIVED 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 progressTracker.currentStep = SENDING
send(otherParty, answers) send(otherParty, answers)
} }

View File

@ -32,7 +32,7 @@ class SimmValuationTest : IntegrationTestCategory {
@Test @Test
fun `runs SIMM valuation demo`() { 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() startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow()
val (nodeA, nodeB) = Futures.allAsList(startNode(nodeALegalName), startNode(nodeBLegalName)).getOrThrow() val (nodeA, nodeB) = Futures.allAsList(startNode(nodeALegalName), startNode(nodeBLegalName)).getOrThrow()
val (nodeAApi, nodeBApi) = Futures.allAsList(startWebserver(nodeA), startWebserver(nodeB)) val (nodeAApi, nodeBApi) = Futures.allAsList(startWebserver(nodeA), startWebserver(nodeB))

View File

@ -20,6 +20,7 @@ include 'experimental:sandbox'
include 'experimental:quasar-hook' include 'experimental:quasar-hook'
include 'verifier' include 'verifier'
include 'test-utils' include 'test-utils'
include 'smoke-test-utils'
include 'tools:explorer' include 'tools:explorer'
include 'tools:explorer:capsule' include 'tools:explorer:capsule'
include 'tools:demobench' include 'tools:demobench'

View File

@ -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')
}

View File

@ -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.crypto.commonName
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.nodeapi.User import net.corda.nodeapi.User
@ -18,13 +22,13 @@ class NodeConfig(
val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) 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, * The configuration object depends upon the networkMap,
* which is mutable. * which is mutable.
*/ */
fun toFileConfig(): Config = ConfigFactory.empty() fun toFileConfig(): Config = empty()
.withValue("myLegalName", valueFor(party.name.toString())) .withValue("myLegalName", valueFor(party.name.toString()))
.withValue("p2pAddress", addressValueFor(p2pPort)) .withValue("p2pAddress", addressValueFor(p2pPort))
.withValue("extraAdvertisedServiceIds", valueFor(extraServices)) .withValue("extraAdvertisedServiceIds", valueFor(extraServices))
@ -42,7 +46,6 @@ class NodeConfig(
private fun <T> valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any) private fun <T> valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any)
private fun addressValueFor(port: Int) = valueFor("localhost:$port") private fun addressValueFor(port: Int) = valueFor("localhost:$port")
private inline fun <T> optional(path: String, obj: T?, body: (Config, T) -> Config): Config { private inline fun <T> optional(path: String, obj: T?, body: (Config, T) -> Config): Config {
val config = ConfigFactory.empty() return if (obj == null) empty() else body(empty(), obj).atPath(path)
return if (obj == null) config else body(config, obj).atPath(path)
} }
} }

View File

@ -1,16 +1,18 @@
package net.corda.kotlin.rpc package net.corda.smoketesting
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.CordaRPCConnection import net.corda.client.rpc.CordaRPCConnection
import net.corda.core.createDirectories
import net.corda.core.div
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import java.io.File
import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths 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.Executors
import java.util.concurrent.TimeUnit.SECONDS import java.util.concurrent.TimeUnit.SECONDS
import kotlin.test.*
class NodeProcess( class NodeProcess(
val config: NodeConfig, val config: NodeConfig,
@ -21,9 +23,7 @@ class NodeProcess(
private companion object { private companion object {
val log = loggerFor<NodeProcess>() val log = loggerFor<NodeProcess>()
val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java") val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java")
val corda = File(this::class.java.getResource("/corda.jar").toURI()) val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(systemDefault())
val buildDir: Path = Paths.get(System.getProperty("build.dir"))
val capsuleDir: Path = buildDir.resolve("capsule")
} }
fun connect(): CordaRPCConnection { fun connect(): CordaRPCConnection {
@ -40,16 +40,20 @@ class NodeProcess(
} }
log.info("Deleting Artemis directories, because they're large!") 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 { 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 { fun create(config: NodeConfig): NodeProcess {
val nodeDir = Files.createTempDirectory(nodesDir, config.commonName) val nodeDir = baseDirectory(config).createDirectories()
log.info("Node directory: {}", nodeDir) log.info("Node directory: {}", nodeDir)
val confFile = nodeDir.resolve("node.conf").toFile() val confFile = nodeDir.resolve("node.conf").toFile()
@ -78,7 +82,7 @@ class NodeProcess(
}, 5, 1, SECONDS) }, 5, 1, SECONDS)
val setupOK = setupExecutor.awaitTermination(120, 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) { } catch (e: Exception) {
process.destroyForcibly() process.destroyForcibly()
throw e throw e
@ -91,17 +95,14 @@ class NodeProcess(
private fun startNode(nodeDir: Path): Process { private fun startNode(nodeDir: Path): Process {
val builder = ProcessBuilder() val builder = ProcessBuilder()
.command(javaPath.toString(), "-jar", corda.path) .command(javaPath.toString(), "-jar", cordaJar.toString())
.directory(nodeDir.toFile()) .directory(nodeDir.toFile())
builder.environment().putAll(mapOf( builder.environment().putAll(mapOf(
"CAPSULE_CACHE_DIR" to capsuleDir.toString() "CAPSULE_CACHE_DIR" to (buildDirectory / "capsule").toString()
)) ))
return builder.start() return builder.start()
} }
} }
} }
private fun File.forceDirectory(): Boolean = this.isDirectory || this.mkdirs()