diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 7871abe352..73ece5801a 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -12,9 +12,11 @@ + + @@ -73,6 +75,8 @@ + + @@ -169,6 +173,8 @@ + + diff --git a/experimental/behave/build.gradle b/experimental/behave/build.gradle index 434f737edc..3eba8624bc 100644 --- a/experimental/behave/build.gradle +++ b/experimental/behave/build.gradle @@ -31,6 +31,7 @@ group 'net.corda.behave' apply plugin: 'java' apply plugin: 'kotlin' +apply plugin: 'net.corda.plugins.publish-utils' sourceCompatibility = 1.8 @@ -43,15 +44,30 @@ sourceSets { java { compileClasspath += main.output runtimeClasspath += main.output - srcDirs = ["src/main/kotlin", "src/scenario/kotlin"] + srcDirs = ['src/main/kotlin', 'src/scenario/kotlin'] } resources.srcDir file('src/scenario/resources') } + smokeTest { + kotlin { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/smoke-test/kotlin') + } + resources { + srcDirs = ['src/scenario/resources'] + } + } } configurations { behaveCompile.extendsFrom testCompile behaveRuntime.extendsFrom testRuntime + + smokeTestCompile.extendsFrom testCompile + smokeTestRuntime.extendsFrom testRuntime + + testArtifacts.extendsFrom behaveRuntime } dependencies { @@ -91,13 +107,16 @@ dependencies { compile project(':node-api') compile project(':client:rpc') - // Unit Tests + // dependency on CordaHttpToRPC proxy + compile project(':testing:qa:behave:tools:rpc-proxy') + testCompile project(':test-utils') + + // Unit Tests testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:$assertj_version" // Scenarios / End-to-End Tests - behaveCompile "info.cukes:cucumber-java8:$cucumber_version" behaveCompile "info.cukes:cucumber-junit:$cucumber_version" behaveCompile "info.cukes:cucumber-picocontainer:$cucumber_version" @@ -115,6 +134,11 @@ test { testLogging.showStandardStreams = true } +task smokeTest(type: Test) { + testClassesDirs = sourceSets.smokeTest.output.classesDirs + classpath = sourceSets.smokeTest.runtimeClasspath +} + task behaveJar(type: Jar) { baseName "corda-behave" from sourceSets.behave.output @@ -133,3 +157,24 @@ task behaveJar(type: Jar) { attributes 'Main-Class': 'net.corda.behave.scenarios.ScenarioRunner' } } + +task apiJar(type: Jar, dependsOn: classes) { + baseName "corda-behave-api" + from sourceSets.behave.output + from { + configurations.behaveCompile.collect { + it.isDirectory() ? it : zipTree(it) + } + } + from project(':client:rpc').configurations.compile.collect { zipTree it } + with jar + include 'net/corda/behave/scenarios/**' + include 'cucumber/api/**' + include 'io/github/lukehutch/**' + exclude '**/features/**' + exclude '**/scripts/**' +} + +artifacts { + testArtifacts apiJar +} diff --git a/experimental/behave/prepare.sh b/experimental/behave/prepare.sh index bdd41c4a1b..1827506bde 100755 --- a/experimental/behave/prepare.sh +++ b/experimental/behave/prepare.sh @@ -6,7 +6,7 @@ set -x # For example: # corda-master => git clone https://github.com/corda/corda # r3corda-master => git clone https://github.com/corda/enterprise -VERSION=corda-master +VERSION=r3corda-master STAGING_DIR=~/staging CORDA_DIR=${STAGING_DIR}/corda/${VERSION} CORDAPP_DIR=${CORDA_DIR}/apps @@ -18,18 +18,32 @@ mkdir -p ${CORDA_DIR} mkdir -p ${CORDAPP_DIR} mkdir -p ${DRIVERS_DIR} -# Copy Corda capsule into deps +# Copy Corda capsule into staging cd ../.. -./gradlew clean :node:capsule:buildCordaJar :finance:jar +./gradlew :node:capsule:buildCordaJar :finance:jar cp -v $(ls node/capsule/build/libs/corda-*.jar | tail -n1) ${CORDA_DIR}/corda.jar # Copy finance library cp -v $(ls finance/build/libs/corda-finance-*.jar | tail -n1) ${CORDAPP_DIR} +# Copy sample Cordapps +./gradlew samples:simm-valuation-demo:jar +cp -v $(ls samples/simm-valuation-demo/build/libs/simm-valuation-demo-*.jar | tail -n1) ${CORDAPP_DIR} + # Download database drivers curl "https://search.maven.org/remotecontent?filepath=com/h2database/h2/1.4.196/h2-1.4.196.jar" > ${DRIVERS_DIR}/h2-1.4.196.jar curl -L "http://central.maven.org/maven2/org/postgresql/postgresql/42.1.4/postgresql-42.1.4.jar" > ${DRIVERS_DIR}/postgresql-42.1.4.jar +curl -L "https://github.com/Microsoft/mssql-jdbc/releases/download/v6.2.2/mssql-jdbc-6.2.2.jre8.jar" > ${DRIVERS_DIR}/mssql-jdbc-6.2.2.jre8.jar # Build Network Bootstrapper ./gradlew buildBootstrapperJar cp -v $(ls tools/bootstrapper/build/libs/*.jar | tail -n1) ${CORDA_DIR}/network-bootstrapper.jar + +# build and distribute Doorman/NMS +./gradlew network-management:capsule:buildDoormanJAR +cp -v $(ls network-management/capsule/build/libs/doorman-*.jar | tail -n1) ${CORDA_DIR}/doorman.jar + +# build and distribute DB Migration tool +./gradlew tools:dbmigration:shadowJar +cp -v $(ls tools/dbmigration/build/libs/*migration-*.jar | tail -n1) ${CORDA_DIR}/dbmigration.jar + diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseType.kt b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseType.kt index 392fba1822..5c52237362 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseType.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseType.kt @@ -12,6 +12,7 @@ package net.corda.behave.database import net.corda.behave.database.configuration.H2ConfigurationTemplate import net.corda.behave.database.configuration.PostgresConfigurationTemplate +import net.corda.behave.database.configuration.SqlServerConfigurationTemplate import net.corda.behave.node.configuration.Configuration import net.corda.behave.node.configuration.DatabaseConfiguration import net.corda.behave.service.database.H2Service @@ -29,8 +30,18 @@ enum class DatabaseType(val settings: DatabaseSettings) { } ), + SQL_SERVER(DatabaseSettings() + .withDatabase(SqlServerService.database) + .withDriver(SqlServerService.driver) + .withSchema(SqlServerService.schema) + .withUser(SqlServerService.username) + .withConfigTemplate(SqlServerConfigurationTemplate()) + .withServiceInitiator { + PostgreSQLService("postgres-${it.name}", it.database.port, it.database.password) + } + ), + POSTGRES(DatabaseSettings() - .withDatabase(PostgreSQLService.database) .withDriver(PostgreSQLService.driver) .withSchema(PostgreSQLService.schema) .withUser(PostgreSQLService.username) @@ -48,8 +59,9 @@ enum class DatabaseType(val settings: DatabaseSettings) { companion object { - fun fromName(name: String): DatabaseType? = when (name.toLowerCase()) { + fun fromName(name: String): DatabaseType? = when (name.replace("[ _-]".toRegex(), "").toLowerCase()) { "h2" -> H2 + "sqlserver" -> SQL_SERVER "postgres" -> POSTGRES "postgresql" -> POSTGRES else -> null diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/SqlServerConfigurationTemplate.kt b/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/SqlServerConfigurationTemplate.kt new file mode 100644 index 0000000000..6b544c632e --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/SqlServerConfigurationTemplate.kt @@ -0,0 +1,38 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ + +package net.corda.behave.database.configuration + +import net.corda.behave.database.DatabaseConfigurationTemplate +import net.corda.behave.node.configuration.DatabaseConfiguration + +class SqlServerConfigurationTemplate : DatabaseConfigurationTemplate() { + + override val connectionString: (DatabaseConfiguration) -> String + get() = { "jdbc:sqlserver://${it.host}:${it.port};database=${it.database}" } + + override val config: (DatabaseConfiguration) -> String + get() = { + """ + |dataSourceProperties = { + | dataSourceClassName = "com.microsoft.sqlserver.jdbc.SQLServerDataSource" + | dataSource.url = "${connectionString(it)}" + | dataSource.user = "${it.username}" + | dataSource.password = "${it.password}" + |} + |database = { + | initialiseSchema=true + | transactionIsolationLevel = READ_COMMITTED + | schema="${it.schema}" + |} + """ + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/file/FileUtilities.kt b/experimental/behave/src/main/kotlin/net/corda/behave/file/FileUtilities.kt index 81229690a4..f0edd1a84a 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/file/FileUtilities.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/file/FileUtilities.kt @@ -10,6 +10,7 @@ package net.corda.behave.file +import net.corda.core.internal.div import java.nio.file.Path import java.nio.file.Paths @@ -19,3 +20,9 @@ val currentDirectory: Path // location of Corda distributions and Drivers dependencies val stagingRoot: Path get() = System.getProperty("STAGING_ROOT")?.let { Paths.get(it) } ?: currentDirectory + +val doormanConfigDirectory: Path + get() = currentDirectory / "src" / "main" / "resources" / "doorman" + +val tmpDirectory: Path + get() = System.getProperty("TMPDIR")?.let { Paths.get(it) } ?: Paths.get("/tmp") \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/network/Network.kt b/experimental/behave/src/main/kotlin/net/corda/behave/network/Network.kt index e4c84dd7fb..21668c964b 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/network/Network.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/network/Network.kt @@ -11,20 +11,23 @@ package net.corda.behave.network import net.corda.behave.database.DatabaseType -import net.corda.behave.file.LogSource -import net.corda.behave.file.currentDirectory -import net.corda.behave.file.stagingRoot +import net.corda.behave.file.* +import net.corda.behave.monitoring.PatternWatch import net.corda.behave.node.Distribution import net.corda.behave.node.Node import net.corda.behave.node.configuration.NotaryType +import net.corda.behave.process.Command import net.corda.behave.process.JarCommand import net.corda.core.CordaException +import net.corda.core.CordaRuntimeException import net.corda.core.internal.* import net.corda.core.utilities.contextLogger import net.corda.core.utilities.minutes -import org.apache.commons.io.FileUtils +import net.corda.core.utilities.seconds import java.io.Closeable +import java.nio.file.Files import java.nio.file.Path +import java.nio.file.StandardCopyOption import java.time.Duration import java.time.Instant import java.time.ZoneOffset.UTC @@ -46,11 +49,16 @@ class Network private constructor( private var hasError = false + private var isDoormanNMSRunning = false + + private lateinit var doormanNMS: JarCommand + init { targetDirectory.createDirectories() } class Builder internal constructor( + private val networkType: Distribution.Type, private val timeout: Duration ) { @@ -68,7 +76,9 @@ class Network private constructor( distribution: Distribution = Distribution.MASTER, databaseType: DatabaseType = DatabaseType.H2, notaryType: NotaryType = NotaryType.NONE, - issuableCurrencies: List = emptyList() + issuableCurrencies: List = emptyList(), + compatibilityZoneURL: String? = null, + withRPCProxy: Boolean = false ): Builder { return addNode(Node.new() .withName(name) @@ -76,6 +86,8 @@ class Network private constructor( .withDatabaseType(databaseType) .withNotaryType(notaryType) .withIssuableCurrencies(*issuableCurrencies.toTypedArray()) + .withRPCProxy(withRPCProxy) + .withNetworkMap(compatibilityZoneURL) ) } @@ -95,8 +107,11 @@ class Network private constructor( if (!network.configureNodes()) { throw CordaException("Unable to configure nodes in Corda network. Please check logs in $directory") } - network.bootstrapLocalNetwork() + if (networkType == Distribution.Type.R3_CORDA) + network.bootstrapDoorman() + else + network.bootstrapLocalNetwork() return network } } @@ -104,7 +119,7 @@ class Network private constructor( fun copyDatabaseDrivers() { val driverDirectory = (targetDirectory / "libs").createDirectories() log.info("Copying database drivers from $stagingRoot/drivers to $driverDirectory") - FileUtils.copyDirectory((stagingRoot / "drivers").toFile(), driverDirectory.toFile()) + Files.copy((stagingRoot / "drivers"), driverDirectory, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING) } fun configureNodes(): Boolean { @@ -125,8 +140,152 @@ class Network private constructor( } } + /** + * This method performs the configuration steps defined in the R3 Corda Network Management readme: + * https://github.com/corda/enterprise/blob/master/network-management/README.md + * using Local signing and "Auto Approval" mode + */ + private fun bootstrapDoorman() { + + // WARNING!! Need to use the correct bootstrapper + // only if using OS nodes (need to choose the latest version) + val r3node = nodes.values + .find { it.config.distribution.type == Distribution.Type.R3_CORDA } ?: throw CordaRuntimeException("Missing R3 distribution node") + val distribution = r3node.config.distribution + + // Copy over reference configuration files used in bootstrapping + val source = doormanConfigDirectory + val doormanTargetDirectory = targetDirectory / "doorman" + source.toFile().copyRecursively(doormanTargetDirectory.toFile(), true) + + // 1. Create key stores for local signer + + // java -jar doorman-.jar --mode ROOT_KEYGEN + log.info("Doorman target directory: $doormanTargetDirectory") + runCommand(JarCommand(distribution.doormanJar, + arrayOf("--config-file", "$doormanConfigDirectory/node-init.conf", "--mode", "ROOT_KEYGEN", "--trust-store-password", "password"), + doormanTargetDirectory, timeout)) + + // java -jar doorman-.jar --mode CA_KEYGEN + runCommand(JarCommand(distribution.doormanJar, + arrayOf("--config-file", "$doormanConfigDirectory/node-init.conf", "--mode", "CA_KEYGEN"), + doormanTargetDirectory, timeout)) + + // 2. Start the doorman service for notary registration + doormanNMS = JarCommand(distribution.doormanJar, + arrayOf("--config-file", "$doormanConfigDirectory/node-init.conf"), + doormanTargetDirectory, timeout) + + val doormanCommand = runCommand(doormanNMS, noWait = true) + log.info("Waiting for DoormanNMS to be alive") + + PatternWatch(doormanCommand.output, "Network management web services started on").await(30.seconds) + log.info("DoormanNMS up and running") + + // Notary Nodes + val notaryNodes = nodes.values.filter { it.config.notary.notaryType != NotaryType.NONE } + notaryNodes.forEach { notaryNode -> + val notaryTargetDirectory = targetDirectory / notaryNode.config.name + log.info("Notary target directory: $notaryTargetDirectory") + + // 3. Create notary node and register with the doorman + runCommand(JarCommand(distribution.cordaJar, + arrayOf("--initial-registration", + "--base-directory", "$notaryTargetDirectory", + "--network-root-truststore", "../doorman/certificates/distribute-nodes/network-root-truststore.jks", + "--network-root-truststore-password", "password"), + notaryTargetDirectory, timeout)) + + // 4. Generate node info files for notary nodes + runCommand(JarCommand(distribution.cordaJar, + arrayOf("--just-generate-node-info", + "--base-directory", "$notaryTargetDirectory"), + notaryTargetDirectory, timeout)) + + // cp (or ln -s) nodeInfo* notary-node-info + val nodeInfoFile = notaryTargetDirectory.toFile().listFiles { _, filename -> filename.matches("nodeInfo-.+".toRegex()) }.firstOrNull() ?: throw CordaRuntimeException("Missing notary nodeInfo file") + + Files.copy(nodeInfoFile.toPath(), (notaryTargetDirectory / "notary-node-info"), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING) + } + + // exit Doorman process + doormanCommand.interrupt() + doormanCommand.waitFor() + + // 5. Add notary identities to the network parameters + + // 6. Load initial network parameters file for network map service + val networkParamsConfig = if (notaryNodes.isEmpty()) "network-parameters-without-notary.conf" else "network-parameters.conf" + val updateNetworkParams = JarCommand(distribution.doormanJar, + arrayOf("--config-file", "$doormanTargetDirectory/node.conf", "--set-network-parameters", "$doormanTargetDirectory/$networkParamsConfig"), + doormanTargetDirectory, timeout) + runCommand(updateNetworkParams) + + // 7. Start a fully configured Doorman / NMS + doormanNMS = JarCommand(distribution.doormanJar, + arrayOf("--config-file", "$doormanConfigDirectory/node.conf"), + doormanTargetDirectory, timeout) + + val doormanNMSCommand = runCommand(doormanNMS, noWait = true) + log.info("Waiting for DoormanNMS to be alive") + + PatternWatch(doormanNMSCommand.output, "Network management web services started on").await(30.seconds) + log.info("DoormanNMS up and running") + + // 8. Register other participant nodes + val partyNodes = nodes.values.filter { it.config.notary.notaryType == NotaryType.NONE } + partyNodes.forEach { partyNode -> + val partyTargetDirectory = targetDirectory / partyNode.config.name + log.info("Party target directory: $partyTargetDirectory") + + // 3. Create notary node and register with the doorman + runCommand(JarCommand(distribution.cordaJar, + arrayOf("--initial-registration", + "--network-root-truststore", "../doorman/certificates/distribute-nodes/network-root-truststore.jks", + "--network-root-truststore-password", "password", + "--base-directory", "$partyTargetDirectory"), + partyTargetDirectory, timeout)) + } + + isDoormanNMSRunning = true + } + + private fun runCommand(command: Command, noWait: Boolean = false): Command { + if (command is JarCommand) { + log.info("Checking existence of jar file:", command.jarFile) + if (!command.jarFile.exists()) { + throw IllegalStateException("Jar file does not exist: ${command.jarFile}") + } + } + log.info("Running command: {}", command) + command.output.subscribe { + if (it.contains("Exception")) { + log.warn("Found error in output; interrupting command execution ...\n{}", it) + command.interrupt() + } + } + command.start() + if (!noWait) { + if (!command.waitFor()) { + hasError = true + error("Failed to execute command") { + val matches = LogSource(targetDirectory) + .find(".*[Ee]xception.*") + .groupBy { it.filename.toAbsolutePath() } + for (match in matches) { + log.info("Log(${match.key}):\n${match.value.joinToString("\n") { it.contents }}") + } + } + } else { + log.info("Command executed successfully") + } + } + return command + } + private fun bootstrapLocalNetwork() { val bootstrapper = nodes.values + .filter { it.config.distribution.type != Distribution.Type.R3_CORDA } .sortedByDescending { it.config.distribution.version } .first() .config.distribution.networkBootstrapper @@ -143,26 +302,25 @@ class Network private constructor( targetDirectory, timeout ) - log.info("Running command: {}", command) - command.output.subscribe { - if (it.contains("Exception")) { - log.warn("Found error in output; interrupting bootstrapping action ...\n{}", it) - command.interrupt() - } + runCommand(command) + } + + private fun bootstrapRPCProxy(node: Node) { + val cordaDistribution = node.config.distribution.path + val rpcProxyPortNo = node.config.nodeInterface.rpcProxy + + val startProxyScript = cordaDistribution / "startRPCproxy.sh" + if (startProxyScript.exists()) { + log.info("Bootstrapping RPC proxy, please wait ...") + val rpcProxyLogFile = targetDirectory / node.config.name / "logs" / "startRPCproxy.log" + val rpcProxyCommand = Command(listOf("$startProxyScript", "$cordaDistribution", "$rpcProxyPortNo", ">>$rpcProxyLogFile", "2>&1"), + cordaDistribution, + timeout + ) + runCommand(rpcProxyCommand) } - command.start() - if (!command.waitFor()) { - hasError = true - error("Failed to bootstrap network") { - val matches = LogSource(targetDirectory) - .find(".*[Ee]xception.*") - .groupBy { it.filename.toAbsolutePath() } - for (match in matches) { - log.info("Log(${match.key}):\n${match.value.joinToString("\n") { it.contents }}") - } - } - } else { - log.info("Network set-up completed") + else { + log.warn("Missing RPC proxy startup script. Continuing ...") } } @@ -205,6 +363,8 @@ class Network private constructor( for (node in nodes.values) { log.info("Starting node [{}]", node.config.name) node.start() + if (node.rpcProxy) + bootstrapRPCProxy(node) } } @@ -269,8 +429,28 @@ class Network private constructor( log.info("Shutting down nodes ...") for (node in nodes.values) { node.shutDown() + if (node.rpcProxy) { + log.info("Shutting down RPC proxy ...") + try { + val rpcProxyPortNo = node.config.nodeInterface.rpcProxy + val pid = Files.lines(tmpDirectory / "rpcProxy-pid-$rpcProxyPortNo").findFirst().get() + // TODO: consider generic implementation to support non *nix platforms + Command(listOf("kill", "-9", "$pid")).run() + (tmpDirectory / "rpcProxy-pid-$rpcProxyPortNo").deleteIfExists() + } + catch (e: Exception) { + log.warn("Unable to locate PID file: ${e.message}") + } + } } - cleanup() + + if (isDoormanNMSRunning) { + log.info("Shutting down R3 Corda NMS server ...") + doormanNMS.kill() + } + + if (System.getProperty("DISABLE_CLEANUP") == null) // useful for re-starting and troubleshooting failure issues + cleanup() } fun use(action: (Network) -> Unit) { @@ -295,7 +475,7 @@ class Network private constructor( val log = contextLogger() const val CLEANUP_ON_ERROR = false - fun new(timeout: Duration = 2.minutes - ): Builder = Builder(timeout) + fun new(type: Distribution.Type = Distribution.Type.CORDA, timeout: Duration = 2.minutes + ): Builder = Builder(type, timeout) } } \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/Distribution.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/Distribution.kt index c2f23e5e8b..eff9eb020d 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/node/Distribution.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/Distribution.kt @@ -11,6 +11,7 @@ package net.corda.behave.node import net.corda.behave.file.stagingRoot +import net.corda.core.CordaRuntimeException import net.corda.core.internal.copyTo import net.corda.core.internal.createDirectories import net.corda.core.internal.div @@ -24,6 +25,11 @@ import java.nio.file.Path */ class Distribution private constructor( + /** + * The distribution type of Corda: Open Source or R3 Corda (Enterprise) + */ + val type: Type, + /** * The version string of the Corda distribution. */ @@ -65,6 +71,16 @@ class Distribution private constructor( */ val networkBootstrapper: Path = path / "network-bootstrapper.jar" + /** + * The path to the doorman jar (R3 Corda only). + */ + val doormanJar: Path = path / "doorman.jar" + + /** + * The path to the DB migration jar (R3 Corda only). + */ + val dbMigrationJar: Path = nodePrefix / version / "dbmigration.jar" + /** * Ensure that the distribution is available on disk. */ @@ -89,6 +105,11 @@ class Distribution private constructor( */ override fun toString() = "Corda(version = $version, path = $cordaJar)" + enum class Type { + CORDA, + R3_CORDA + } + companion object { private val log = contextLogger() @@ -97,38 +118,46 @@ class Distribution private constructor( private val nodePrefix = stagingRoot / "corda" - val MASTER = fromJarFile("corda-master") + val MASTER = fromJarFile(Type.CORDA, "corda-master") + val R3_MASTER = fromJarFile(Type.R3_CORDA, "r3corda-master") /** * Get representation of a Corda distribution from Artifactory based on its version string. + * @param type The Corda distribution type. * @param version The version of the Corda distribution. */ - fun fromArtifactory(version: String): Distribution { - val url = URL("https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases/net/corda/corda/$version/corda-$version.jar") + fun fromArtifactory(type: Type, version: String): Distribution { + val url = + when (type) { + Type.CORDA -> URL("https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases/net/corda/corda/$version/corda-$version.jar") + Type.R3_CORDA -> URL("https://ci-artifactory.corda.r3cev.com/artifactory/r3-corda-releases/com/r3/corda/corda/$version/corda-$version.jar") + } log.info("Artifactory URL: $url\n") - val distribution = Distribution(version, url = url) + val distribution = Distribution(type, version, url = url) distributions.add(distribution) return distribution } /** * Get representation of a Corda distribution based on its version string and fat JAR path. + * @param type The Corda distribution type. * @param version The version of the Corda distribution. * @param jarFile The path to the Corda fat JAR. */ - fun fromJarFile(version: String, jarFile: Path? = null): Distribution { - val distribution = Distribution(version, file = jarFile) + fun fromJarFile(type: Type, version: String, jarFile: Path? = null): Distribution { + val distribution = Distribution(type, version, file = jarFile) distributions.add(distribution) return distribution } /** * Get Corda distribution from a Docker image file. + * @param type The Corda distribution type. * @param baseImage The name (eg. corda) of the Corda distribution. * @param imageTag The version (github commit id or corda version) of the Corda distribution. */ - fun fromDockerImage(baseImage: String, imageTag: String): Distribution { - val distribution = Distribution(version = imageTag, baseImage = baseImage) + fun fromDockerImage(type: Type, baseImage: String, imageTag: String): Distribution { + val distribution = Distribution(type, version = imageTag, baseImage = baseImage) distributions.add(distribution) return distribution } @@ -139,8 +168,11 @@ class Distribution private constructor( */ fun fromVersionString(version: String): Distribution = when (version) { "master" -> MASTER - "corda-3.0" -> fromArtifactory(version) - else -> fromJarFile(version) + "r3-master" -> R3_MASTER + "corda-3.0" -> fromArtifactory(Type.CORDA, version) + "corda-3.1" -> fromArtifactory(Type.CORDA, version) + "R3.CORDA-3.0.0-DEV-PREVIEW-3" -> fromArtifactory(Type.R3_CORDA, version) + else -> distributions.firstOrNull { it.version == version } ?: throw CordaRuntimeException("Unable to locate Corda distribution for version: $version") } } } diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt index b7d132a9b4..475ae383bd 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt @@ -18,16 +18,20 @@ import net.corda.behave.file.stagingRoot import net.corda.behave.monitoring.PatternWatch import net.corda.behave.node.configuration.* import net.corda.behave.process.JarCommand +import net.corda.behave.process.JarCommandWithMain import net.corda.behave.service.Service import net.corda.behave.service.ServiceSettings +import net.corda.behave.service.proxy.CordaRPCProxyClient import net.corda.behave.ssh.MonitoringSSHClient import net.corda.behave.ssh.SSHClient import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClientConfiguration +import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.exists import net.corda.core.messaging.CordaRPCOps import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.minutes import net.corda.core.utilities.loggerFor import net.corda.core.utilities.seconds import org.apache.commons.io.FileUtils @@ -42,7 +46,9 @@ import java.util.concurrent.CountDownLatch class Node( val config: Configuration, private val rootDirectory: Path = currentDirectory, - private val settings: ServiceSettings = ServiceSettings() + private val settings: ServiceSettings = ServiceSettings(), + val rpcProxy: Boolean = false, + val networkType: Distribution.Type ) { private val log = loggerFor() @@ -89,16 +95,40 @@ class Node( log.info("Configuring {} ...", this) serviceDependencies.addAll(config.database.type.dependencies(config)) config.distribution.ensureAvailable() - config.writeToFile(rootDirectory / "${config.name}_node.conf") + if (networkType == Distribution.Type.CORDA) { + config.writeToFile(rootDirectory / "${config.name}_node.conf") + } + else { + val nodeDirectory = (rootDirectory / config.name).createDirectories() + config.writeToFile(nodeDirectory / "node.conf") + } installApps() } + private fun initialiseDatabase(database: DatabaseConfiguration) { + val driversDir = runtimeDirectory / "drivers" + log.info("Creating directory for drivers: $driversDir") + driversDir.toFile().mkdirs() + log.info("Initialising database for R3 Corda node: $database") + val command = JarCommandWithMain(listOf(config.distribution.dbMigrationJar, rootDirectory / "libs" / database.type.driverJar!!), + "com.r3.corda.dbmigration.DBMigration", + arrayOf("--base-directory", "$runtimeDirectory", "--execute-migration"), + runtimeDirectory, 2.minutes) + command.run() + } + fun start(): Boolean { if (!startDependencies()) { return false } log.info("Starting {} ...", this) return try { + // initialise database via DB migration tool + if (config.distribution.type == Distribution.Type.R3_CORDA && + config.database.type != DatabaseType.H2) { + initialiseDatabase(config.database) + } + // launch node itself command.start() isStarted = true true @@ -182,6 +212,20 @@ class Node( return result ?: error("Failed to run RPC action") } + fun http(action: (CordaRPCOps) -> T): T { + val address = config.nodeInterface + val targetHost = NetworkHostAndPort(address.host, address.rpcProxy) + log.info("Establishing HTTP connection to ${targetHost.host} on port ${targetHost.port} ...") + try { + return action(CordaRPCProxyClient(targetHost)) + } + catch (e: Exception) { + log.warn("Failed to invoke http endpoint: ", e) + e.printStackTrace() + error("Failed to run http action") + } + } + override fun toString(): String { return "Node(name = ${config.name}, version = ${config.distribution.version})" } @@ -248,6 +292,8 @@ class Node( private var notaryType = NotaryType.NONE + private var compatibilityZoneURL: String? = null + private val issuableCurrencies = mutableListOf() private var location: String = "London" @@ -262,6 +308,10 @@ class Node( private var timeout = Duration.ofSeconds(60) + private var rpcProxy = false + + var networkType = distribution.type + fun withName(newName: String): Builder { name = newName return this @@ -269,6 +319,7 @@ class Node( fun withDistribution(newDistribution: Distribution): Builder { distribution = newDistribution + networkType = distribution.type return this } @@ -282,6 +333,16 @@ class Node( return this } + fun withNetworkMap(newCompatibilityZoneURL: String?): Builder { + compatibilityZoneURL = newCompatibilityZoneURL + return this + } + + fun withNetworkType(newNetworkType: Distribution.Type): Builder { + networkType = newNetworkType + return this + } + fun withIssuableCurrencies(vararg currencies: String): Builder { issuableCurrencies.addAll(currencies) return this @@ -318,9 +379,18 @@ class Node( return this } + fun withRPCProxy(withRPCProxy: Boolean): Builder { + rpcProxy = withRPCProxy + return this + } + fun build(): Node { val name = name ?: error("Node name not set") val directory = directory ?: error("Runtime directory not set") + val compatibilityZoneURL = + if (networkType == Distribution.Type.R3_CORDA) + compatibilityZoneURL ?: "http://localhost:1300" + else null return Node( Configuration( name, @@ -335,11 +405,14 @@ class Node( ), configElements = *arrayOf( NotaryConfiguration(notaryType), + NetworkMapConfiguration(compatibilityZoneURL), CurrencyConfiguration(issuableCurrencies) ) ), directory, - ServiceSettings(timeout) + ServiceSettings(timeout), + rpcProxy = rpcProxy, + networkType = networkType ) } diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt index 494b595541..42bb277775 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt @@ -36,7 +36,7 @@ class Configuration( vararg configElements: ConfigurationTemplate ) { - private val developerMode = true + private val developerMode = (distribution.type == Distribution.Type.CORDA) val cordaX500Name: CordaX500Name by lazy({ CordaX500Name(name, location, country) diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NetworkInterface.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NetworkInterface.kt index e358da148f..265cab404f 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NetworkInterface.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NetworkInterface.kt @@ -18,6 +18,7 @@ data class NetworkInterface( val sshPort: Int = getPort(2222 + nodeIndex), val p2pPort: Int = getPort(12001 + (nodeIndex * 5)), val rpcPort: Int = getPort(12002 + (nodeIndex * 5)), + val rpcProxy: Int = getPort(13002 + (nodeIndex * 5)), val rpcAdminPort: Int = getPort(12003 + (nodeIndex * 5)), val webPort: Int = getPort(12004 + (nodeIndex * 5)), val dbPort: Int = getPort(12005 + (nodeIndex * 5)), diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NetworkMapConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NetworkMapConfiguration.kt new file mode 100644 index 0000000000..e6c5ff632e --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NetworkMapConfiguration.kt @@ -0,0 +1,15 @@ +package net.corda.behave.node.configuration + +class NetworkMapConfiguration(private val compatibilityZoneURL: String? = null) : ConfigurationTemplate() { + + override val config: (Configuration) -> String + get() = { + if (compatibilityZoneURL != null) { + """ + |compatibilityZoneURL="$compatibilityZoneURL" + """ + } else { + "" + } + } +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryConfiguration.kt index 63a82ece68..42ba7c11ee 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryConfiguration.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryConfiguration.kt @@ -10,7 +10,7 @@ package net.corda.behave.node.configuration -class NotaryConfiguration(private val notaryType: NotaryType = NotaryType.NONE) : ConfigurationTemplate() { +class NotaryConfiguration(val notaryType: NotaryType = NotaryType.NONE) : ConfigurationTemplate() { override val config: (Configuration) -> String get() = { diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/process/JarCommand.kt b/experimental/behave/src/main/kotlin/net/corda/behave/process/JarCommand.kt index 299595dbaf..f72cf88f67 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/process/JarCommand.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/process/JarCommand.kt @@ -14,7 +14,7 @@ import java.nio.file.Path import java.time.Duration class JarCommand( - jarFile: Path, + val jarFile: Path, arguments: Array, directory: Path, timeout: Duration, diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/process/JarCommandWithMain.kt b/experimental/behave/src/main/kotlin/net/corda/behave/process/JarCommandWithMain.kt new file mode 100644 index 0000000000..a2c2be9eba --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/process/JarCommandWithMain.kt @@ -0,0 +1,36 @@ +package net.corda.behave.process + +import java.nio.file.Path +import java.time.Duration + +class JarCommandWithMain( + jarFiles: List, + mainClass: String, + arguments: Array, + directory: Path, + timeout: Duration, + enableRemoteDebugging: Boolean = false +) : Command( + command = listOf( + "/usr/bin/java", + *extraArguments(enableRemoteDebugging), + "-cp", "${jarFiles.joinToString(":")}", + mainClass, + *arguments + ), + directory = directory, + timeout = timeout +) { + + companion object { + + private fun extraArguments(enableRemoteDebugging: Boolean) = + if (enableRemoteDebugging) { + arrayOf("-Dcapsule.jvm.args=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005") + } else { + arrayOf() + } + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/database/SqlServerService.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/database/SqlServerService.kt new file mode 100644 index 0000000000..4e3601a164 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/database/SqlServerService.kt @@ -0,0 +1,57 @@ + +import net.corda.behave.database.DatabaseConnection +import net.corda.behave.database.DatabaseType +import net.corda.behave.database.configuration.SqlServerConfigurationTemplate +import net.corda.behave.node.configuration.DatabaseConfiguration +import net.corda.behave.service.ContainerService +import net.corda.behave.service.ServiceSettings + +class SqlServerService( + name: String, + port: Int, + private val password: String, + settings: ServiceSettings = ServiceSettings() +) : ContainerService(name, port, "SQL Server is now ready for client connections", settings) { + + override val baseImage = "microsoft/mssql-server-linux" + + override val internalPort = 1433 + + init { + addEnvironmentVariable("ACCEPT_EULA", "Y") + addEnvironmentVariable("SA_PASSWORD", password) + } + + override fun verify(): Boolean { + val config = DatabaseConfiguration( + type = DatabaseType.SQL_SERVER, + host = host, + port = port, + database = database, + schema = schema, + username = username, + password = password + ) + val connection = DatabaseConnection(config, SqlServerConfigurationTemplate()) + try { + connection.use { + return true + } + } catch (ex: Exception) { + log.warn(ex.message, ex) + ex.printStackTrace() + } + return false + } + + companion object { + + val host = "localhost" + val database = "master" + val schema = "dbo" + val username = "sa" + val driver = "mssql-jdbc-6.2.2.jre8.jar" + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/resources/doorman/network-parameters-without-notary.conf b/experimental/behave/src/main/resources/doorman/network-parameters-without-notary.conf new file mode 100644 index 0000000000..1f47b7f021 --- /dev/null +++ b/experimental/behave/src/main/resources/doorman/network-parameters-without-notary.conf @@ -0,0 +1,3 @@ +minimumPlatformVersion = 1 +maxMessageSize = 10485760 +maxTransactionSize = 10485760 diff --git a/experimental/behave/src/main/resources/doorman/network-parameters.conf b/experimental/behave/src/main/resources/doorman/network-parameters.conf new file mode 100644 index 0000000000..a34049e6cb --- /dev/null +++ b/experimental/behave/src/main/resources/doorman/network-parameters.conf @@ -0,0 +1,7 @@ +notaries : [{ + notaryNodeInfoFile: "../Notary/notary-node-info" + validating: false + }] + minimumPlatformVersion = 1 + maxMessageSize = 10485760 + maxTransactionSize = 10485760 diff --git a/experimental/behave/src/main/resources/doorman/node-init.conf b/experimental/behave/src/main/resources/doorman/node-init.conf new file mode 100644 index 0000000000..dbc3935d1e --- /dev/null +++ b/experimental/behave/src/main/resources/doorman/node-init.conf @@ -0,0 +1,34 @@ +basedir = "." +address = "localhost:1300" + +#For local signing +rootStorePath = ${basedir}"/certificates/rootstore.jks" +keystorePath = ${basedir}"/certificates/caKeystore.jks" +keystorePassword = "password" +caPrivateKeyPassword = "password" +rootKeystorePassword = "password" +rootPrivateKeyPassword = "password" + +# Database config +dataSourceProperties { + dataSourceClassName = org.h2.jdbcx.JdbcDataSource + "dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port} + "dataSource.user" = sa + "dataSource.password" = "" +} +database { runMigration = true } +h2port = 0 + +# Doorman config +# Comment out this section if running without doorman service +doorman { + approveInterval = 20 + approveAll = true +} + +# Network map config +# Comment out this section if running without network map service +#networkMap { +# cacheTimeout = 1000 +# signInterval = 3000 +#} diff --git a/experimental/behave/src/main/resources/doorman/node.conf b/experimental/behave/src/main/resources/doorman/node.conf new file mode 100644 index 0000000000..7c229ca01e --- /dev/null +++ b/experimental/behave/src/main/resources/doorman/node.conf @@ -0,0 +1,32 @@ +basedir = "." +address = "localhost:1300" + +#For local signing +rootStorePath = ${basedir}"/certificates/rootstore.jks" +keystorePath = ${basedir}"/certificates/caKeystore.jks" +keystorePassword = "password" +caPrivateKeyPassword = "password" + +# Database config +dataSourceProperties { + dataSourceClassName = org.h2.jdbcx.JdbcDataSource + "dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port} + "dataSource.user" = sa + "dataSource.password" = "" +} +database { runMigration = true } +h2port = 0 + +# Doorman config +# Comment out this section if running without doorman service +doorman { + approveInterval = 20 + approveAll = true +} + +# Network map config +# Comment out this section if running without network map service +networkMap { + cacheTimeout = 1000 + signInterval = 3000 +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt index 0068b9bb63..fc3b14c423 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt @@ -12,6 +12,7 @@ package net.corda.behave.scenarios import cucumber.api.java.After import net.corda.behave.network.Network +import net.corda.behave.node.Distribution import net.corda.behave.node.Node import net.corda.core.messaging.CordaRPCOps import org.assertj.core.api.Assertions.assertThat @@ -50,9 +51,15 @@ class ScenarioState { // Network is already running return } - val networkBuilder = Network.new() + + // Corda Network will be configured as R3 Corda (with Doorman/NMS) if any Node uses an R3.Corda distribution + val r3CordaNode = nodes.find { it.networkType == Distribution.Type.R3_CORDA } + val networkType = if (r3CordaNode != null) Distribution.Type.R3_CORDA else Distribution.Type.CORDA + log.info("Corda network type: $networkType") + + val networkBuilder = Network.new(networkType) for (node in nodes) { - networkBuilder.addNode(node) + networkBuilder.addNode(node.withNetworkType(networkType)) } network = networkBuilder.generate() network?.start() @@ -72,6 +79,14 @@ class ScenarioState { } } + inline fun withClientProxy(nodeName: String, crossinline action: (CordaRPCOps) -> T): T { + withNetwork { + return node(nodeName).http { + action(it) + } + } + } + @After fun stopNetwork() { val network = network ?: return diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Substeps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Substeps.kt index b9b7ad8044..1fa04e4d4f 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Substeps.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Substeps.kt @@ -30,4 +30,14 @@ abstract class Substeps(protected val state: ScenarioState) { } }) } + + protected fun withClientProxy(nodeName: String, action: ScenarioState.(CordaRPCOps) -> T): T { + return state.withClientProxy(nodeName, { + return@withClientProxy try { + action(state, it) + } catch (ex: Exception) { + state.error(ex.message ?: "Failed to execute HTTP call") + } + }) + } } \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/ConfigurationSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/ConfigurationSteps.kt index 2b78ed3f1d..44d63a6137 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/ConfigurationSteps.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/ConfigurationSteps.kt @@ -39,6 +39,18 @@ class ConfigurationSteps : StepsBlock { ?: error("Unknown notary type '$notaryType'")) } + Given("^a node (\\w+) of version ([^ ]+) with proxy$") { name, version -> + node(name).withRPCProxy(true) + .withDistribution(Distribution.fromVersionString(version)) + } + + Given("^a (\\w+) notary node (\\w+) of version ([^ ]+) with proxy$") { notaryType, name, version -> + node(name).withRPCProxy(true) + .withDistribution(Distribution.fromVersionString(version)) + .withNotaryType(notaryType.toNotaryType() + ?: error("Unknown notary type '$notaryType'")) + } + Given("^a (\\w+) notary (\\w+) of version ([^ ]+)$") { type, name, version -> node(name) .withDistribution(Distribution.fromVersionString(version)) @@ -70,7 +82,5 @@ class ConfigurationSteps : StepsBlock { Given("^node (\\w+) has app installed: (.+)$") { name, app -> node(name).withApp(app) } - } } - diff --git a/experimental/behave/src/scenario/resources/features/cash/currencies.feature b/experimental/behave/src/scenario/resources/features/cash/currencies.feature index 2b2ae5d285..614f54a81c 100644 --- a/experimental/behave/src/scenario/resources/features/cash/currencies.feature +++ b/experimental/behave/src/scenario/resources/features/cash/currencies.feature @@ -10,7 +10,7 @@ Feature: Cash - Issuable Currencies Examples: | Node-Version | - | master | + | r3-master | Scenario Outline: Node has an issuable currency Given a node PartyA of version @@ -21,7 +21,7 @@ Feature: Cash - Issuable Currencies Examples: | Node-Version | - | master | + | r3-master | Scenario Outline: Node can issue a currency Given a node PartyA of version @@ -32,4 +32,4 @@ Feature: Cash - Issuable Currencies Examples: | Node-Version | - | master | + | r3-master | diff --git a/experimental/behave/src/scenario/resources/features/database/connection.feature b/experimental/behave/src/scenario/resources/features/database/connection.feature index 6bbdf73c09..ad96774114 100644 --- a/experimental/behave/src/scenario/resources/features/database/connection.feature +++ b/experimental/behave/src/scenario/resources/features/database/connection.feature @@ -10,7 +10,8 @@ Feature: Database - Connection Examples: | Node-Version | Database-Type | - | master | H2 | + | r3-master | H2 | -# To run this scenario using postgreSQL you must ensure that Docker is running locally -# | master | postgreSQL | \ No newline at end of file +# To run this scenario using other DB providers you must ensure that Docker is running locally +# | r3-master | postgreSQL | +# | r3-master | SQL Server | \ No newline at end of file diff --git a/experimental/behave/src/scenario/resources/features/startup/logging.feature b/experimental/behave/src/scenario/resources/features/startup/logging.feature index ce4ef490ed..0e0138becc 100644 --- a/experimental/behave/src/scenario/resources/features/startup/logging.feature +++ b/experimental/behave/src/scenario/resources/features/startup/logging.feature @@ -12,7 +12,7 @@ Feature: Startup Information - Logging Examples: | Node-Version | Database-Type | - | master | H2 | + | r3-master | H2 | Scenario Outline: Node shows database details on startup Given a node PartyA of version @@ -22,7 +22,7 @@ Feature: Startup Information - Logging Examples: | Node-Version | Database-Type | - | master | H2 | + | r3-master | H2 | Scenario Outline: Node shows version information on startup Given a node PartyA of version @@ -30,5 +30,19 @@ Feature: Startup Information - Logging And node PartyA is on release version Examples: - | Node-Version | Platform-Version | Release-Version | - | master | 4 | corda-4.0-snapshot | + | Node-Version | Platform-Version | Release-Version | + | r3-master | 4 | R3.CORDA-3.0-SNAPSHOT | + + Scenario Outline: Start-up a simple 3 node network with a non validating notary + Given a node PartyA of version + And a node PartyB of version + And a node PartyC of version + And a nonvalidating notary Notary of version + When the network is ready + Then user can retrieve logging information for node PartyA + And user can retrieve logging information for node PartyB + And user can retrieve logging information for node PartyC + + Examples: + | Node1-Version | Node2-Version | Node3-Version | Notary-Version | + | r3-master | r3-master | r3-master | r3-master | diff --git a/experimental/behave/src/scenario/resources/scripts/run-behave-features.sh b/experimental/behave/src/scenario/resources/scripts/run-behave-features.sh index e016d00107..86220635d4 100755 --- a/experimental/behave/src/scenario/resources/scripts/run-behave-features.sh +++ b/experimental/behave/src/scenario/resources/scripts/run-behave-features.sh @@ -14,7 +14,7 @@ cd ${BUILD_DIR} ../../gradlew behaveJar BEHAVE_JAR=$(ls build/libs/corda-behave-*.jar | tail -n1) -STAGING_ROOT=~/staging +STAGING_ROOT=~/staging # startup java -DSTAGING_ROOT=${STAGING_ROOT} -jar ${BEHAVE_JAR} --glue net.corda.behave.scenarios -path ./src/scenario/resources/features/startup/logging.feature @@ -23,4 +23,4 @@ java -DSTAGING_ROOT=${STAGING_ROOT} -jar ${BEHAVE_JAR} --glue net.corda.behave.s java -DSTAGING_ROOT=${STAGING_ROOT} -jar ${BEHAVE_JAR} --glue net.corda.behave.scenarios -path ./src/scenario/resources/features/cash/currencies.feature # database -java -DSTAGING_ROOT=${STAGING_ROOT} -jar ${BEHAVE_JAR} --glue net.corda.behave.scenarios -path ./src/scenario/resources/features/cash/currencies.feature +java -DSTAGING_ROOT=${STAGING_ROOT} -jar ${BEHAVE_JAR} --glue net.corda.behave.scenarios -path ./src/scenario/resources/features/database/connection.feature diff --git a/experimental/behave/src/smoke-test/kotlin/net/corda/behave/process/DBMigrationToolTests.kt b/experimental/behave/src/smoke-test/kotlin/net/corda/behave/process/DBMigrationToolTests.kt new file mode 100644 index 0000000000..3d242ac613 --- /dev/null +++ b/experimental/behave/src/smoke-test/kotlin/net/corda/behave/process/DBMigrationToolTests.kt @@ -0,0 +1,44 @@ +package net.corda.behave.process + +import net.corda.behave.node.Distribution +import net.corda.core.CordaRuntimeException +import net.corda.core.internal.div +import net.corda.core.utilities.minutes +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.nio.file.Paths + +class DBMigrationToolTests { + + /** + * Commands used to perform Database initialisation and migration as per: + * http://docs.corda.r3.com/website/releases/docs_head/api-persistence.html#database-migration + */ + + // Set corresponding Java properties to point to valid Corda node configurations + // eg. -DNODE_DIR= -DJDBC_DRIVER=postgresql-42.1.4.jar + private val nodeRunDir = Paths.get(System.getProperty("NODE_DIR") ?: throw CordaRuntimeException("Please set NODE_DIR to point to valid Node configuration")) + private val jdbcDriver = nodeRunDir / ".." / "libs" / (System.getProperty("JDBC_DRIVER") ?: throw CordaRuntimeException("Please set JDBC_DRIVER to point to valid JDBC driver jar file located under $nodeRunDir\\..\\libs")) + + private val migrationToolMain = "com.r3.corda.dbmigration.DBMigration" + + @Test + fun `dry run migration`() { + println(nodeRunDir) + val command = JarCommandWithMain(listOf(Distribution.R3_MASTER.dbMigrationJar, jdbcDriver), + migrationToolMain, + arrayOf("--base-directory", "$nodeRunDir", "--dry-run"), + nodeRunDir, 2.minutes) + assertThat(command.run()).isEqualTo(0) + } + + @Test + fun `execute migration`() { + println(nodeRunDir) + val command = JarCommandWithMain(listOf(Distribution.R3_MASTER.dbMigrationJar, jdbcDriver), + migrationToolMain, + arrayOf("--base-directory", "$nodeRunDir", "--execute-migration"), + nodeRunDir, 2.minutes) + assertThat(command.run()).isEqualTo(0) + } +} \ No newline at end of file diff --git a/experimental/behave/src/smoke-test/kotlin/net/corda/behave/process/DoormanCommandTests.kt b/experimental/behave/src/smoke-test/kotlin/net/corda/behave/process/DoormanCommandTests.kt new file mode 100644 index 0000000000..9747f53416 --- /dev/null +++ b/experimental/behave/src/smoke-test/kotlin/net/corda/behave/process/DoormanCommandTests.kt @@ -0,0 +1,110 @@ +package net.corda.behave.process + +import net.corda.behave.file.currentDirectory +import net.corda.behave.file.doormanConfigDirectory +import net.corda.behave.node.Distribution +import net.corda.core.CordaRuntimeException +import net.corda.core.internal.div +import net.corda.core.utilities.minutes +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.nio.file.Paths + +class DoormanCommandTests { + + /** + * Commands that reproduce the setting up of an R3 Corda Network as per instructions in: + * https://github.com/corda/enterprise/blob/master/network-management/README.md + */ + + private val source = doormanConfigDirectory + private val doormanRunDir = currentDirectory / "build" / "runs" / "doorman" + + // Set corresponding Java properties to point to valid Corda node configurations + // eg. -DNOTARY_NODE_DIR= + private val notaryRunDir = Paths.get(System.getProperty("NOTARY_NODE_DIR") ?: throw CordaRuntimeException("Please set NOTARY_NODE_DIR to point to valid Notary node configuration")) + private val nodeRunDir = Paths.get(System.getProperty("NODE_DIR") ?: throw CordaRuntimeException("Please set NODE_DIR to point to valid Node configuration")) + + @Test + fun `step 1 - create key stores for local signer`() { + println(doormanRunDir) + source.toFile().copyRecursively(doormanRunDir.toFile(), true) + + // ROOT + val commandRoot = JarCommand(Distribution.R3_MASTER.doormanJar, + arrayOf("--config-file", "$doormanConfigDirectory/node-init.conf", "--mode", "ROOT_KEYGEN", "--trust-store-password", "password"), + doormanRunDir, 1.minutes) + assertThat(commandRoot.run()).isEqualTo(0) + + // CA + val commandCA = JarCommand(Distribution.R3_MASTER.doormanJar, + arrayOf("--config-file", "$doormanConfigDirectory/node-init.conf", "--mode", "CA_KEYGEN"), + doormanRunDir, 1.minutes) + assertThat(commandCA.run()).isEqualTo(0) + } + + @Test + fun `step 2 - start doorman service for notary registration`() { + println(doormanRunDir) + + val command = JarCommand(Distribution.R3_MASTER.doormanJar, + arrayOf("--config-file", "$doormanConfigDirectory/node-init.conf"), + doormanRunDir, 10.minutes) + assertThat(command.run()).isEqualTo(0) + } + + @Test + fun `step 3 - create notary node and register with the doorman`() { + println(notaryRunDir) + val command = JarCommand(Distribution.R3_MASTER.cordaJar, + arrayOf("--initial-registration", + "--base-directory", "$notaryRunDir", + "--network-root-truststore", "../doorman/certificates/distribute-nodes/network-root-truststore.jks", + "--network-root-truststore-password", "password"), + notaryRunDir, 2.minutes) + assertThat(command.run()).isEqualTo(0) + } + + @Test + fun `step 4 - generate node info files for notary nodes`() { + println(notaryRunDir) + val command = JarCommand(Distribution.R3_MASTER.cordaJar, + arrayOf("--just-generate-node-info", + "--base-directory", "$notaryRunDir"), + doormanRunDir, 1.minutes) + assertThat(command.run()).isEqualTo(0) + } + + // step 5 Add notary identities to the network parameters + // (already configured in template network parameters file) + + @Test + fun `step 6 - load initial network parameters file for network map service`() { + println(doormanRunDir) + val command = JarCommand(Distribution.R3_MASTER.doormanJar, + arrayOf("--config-file", "$doormanRunDir/node.conf", "--set-network-parameters", "$doormanRunDir/network-parameters.conf"), + doormanRunDir, 1.minutes) + assertThat(command.run()).isEqualTo(0) + } + + @Test + fun `step 7 - Start a fully configured Doorman NMS`() { + println(doormanRunDir) + val command = JarCommand(Distribution.R3_MASTER.doormanJar, + arrayOf("--config-file", "$doormanRunDir/node.conf"), + doormanRunDir, 1.minutes) + assertThat(command.run()).isEqualTo(0) + } + + @Test + fun `step 8 - initial registration of network participant nodes`() { + println(nodeRunDir) + val command = JarCommand(Distribution.R3_MASTER.cordaJar, + arrayOf("--initial-registration", + "--network-root-truststore", "../doorman/certificates/distribute-nodes/network-root-truststore.jks", + "--network-root-truststore-password", "password", + "--base-directory", "$nodeRunDir"), + doormanRunDir, 2.minutes) + assertThat(command.run()).isEqualTo(0) + } +} \ No newline at end of file diff --git a/experimental/behave/src/smoke-test/kotlin/net/corda/behave/process/RPCProxyCommandTests.kt b/experimental/behave/src/smoke-test/kotlin/net/corda/behave/process/RPCProxyCommandTests.kt new file mode 100644 index 0000000000..c9d5358ec9 --- /dev/null +++ b/experimental/behave/src/smoke-test/kotlin/net/corda/behave/process/RPCProxyCommandTests.kt @@ -0,0 +1,33 @@ +package net.corda.behave.process + +import net.corda.behave.file.tmpDirectory +import net.corda.behave.node.Distribution +import net.corda.core.internal.delete +import net.corda.core.internal.div +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.nio.file.Files + +class RPCProxyCommandTests { + + /** + * Ensure you have configured the environment correctly by running the "prepare.sh" script + * and then use a JVM option to ensure that STAGING_ROOT points to corresponding location used in the above script. + * eg. + * -ea -DSTAGING_ROOT=/home/staging + */ + + @Test + fun `successful launch rpc proxy`() { + val cordaDistribution = Distribution.MASTER.path + val portNo = 13000 + val command = Command(listOf("$cordaDistribution/startRPCproxy.sh", "$cordaDistribution", "$portNo")) + val exitCode = command.run() + assertThat(exitCode).isEqualTo(0) + + val pid = Files.lines(tmpDirectory / "rpcProxy-pid-$portNo").findFirst().get() + println("Killing RPCProxyServer with pid: $pid") + Command(listOf("kill", "-9", "$pid")).run() + (tmpDirectory / "rpcProxy-pid-$portNo").delete() + } +} \ No newline at end of file diff --git a/experimental/behave/src/smoke-test/kotlin/net/corda/behave/service/proxy/CordaRPCProxyClientTest.kt b/experimental/behave/src/smoke-test/kotlin/net/corda/behave/service/proxy/CordaRPCProxyClientTest.kt new file mode 100644 index 0000000000..1d6f7caf36 --- /dev/null +++ b/experimental/behave/src/smoke-test/kotlin/net/corda/behave/service/proxy/CordaRPCProxyClientTest.kt @@ -0,0 +1,54 @@ +package net.corda.behave.service.proxy + +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.finance.POUNDS +import net.corda.finance.flows.CashIssueFlow +import org.assertj.core.api.Assertions +import org.junit.Test + +abstract class CordaRPCProxyClientTest { + + companion object { + private val RPC_PROXY_SERVER_PORT = 13002 + private val rpcProxyHostAndPort = NetworkHostAndPort("localhost", RPC_PROXY_SERVER_PORT) + } + + @Test + fun startFlowDynamic() { + val rpcProxyClient = CordaRPCProxyClient(rpcProxyHostAndPort) + val notary = rpcProxyClient.notaryIdentities()[0] + val response = rpcProxyClient.startFlow(::CashIssueFlow, POUNDS(100), OpaqueBytes.of(1), notary).returnValue.getOrThrow() + println(response) + Assertions.assertThat(response.stx.toString()).matches("SignedTransaction\\(id=.*\\)") + } + + @Test + fun nodeInfo() { + val rpcProxyClient = CordaRPCProxyClient(rpcProxyHostAndPort) + val response = rpcProxyClient.nodeInfo() + println(response) + Assertions.assertThat(response.toString()).matches("NodeInfo\\(addresses=\\[.*\\], legalIdentitiesAndCerts=\\[.*\\], platformVersion=.*, serial=.*\\)") + } + + @Test + fun notaryIdentities() { + val rpcProxyClient = CordaRPCProxyClient(rpcProxyHostAndPort) + val response = rpcProxyClient.notaryIdentities() + println(response) + Assertions.assertThat(response.first().name.toString()).isEqualTo("O=Notary, L=London, C=GB") + } + + @Test + fun registeredFlows() { + val rpcProxyClient = CordaRPCProxyClient(rpcProxyHostAndPort) + val response = rpcProxyClient.registeredFlows() + println(response) + // Node built-in flows + Assertions.assertThat(response).contains("net.corda.core.flows.ContractUpgradeFlow\$Authorise", + "net.corda.core.flows.ContractUpgradeFlow\$Deauthorise", + "net.corda.core.flows.ContractUpgradeFlow\$Initiate") + } +} \ No newline at end of file diff --git a/experimental/behave/src/smoke-test/kotlin/net/corda/behave/service/proxy/OSNetworkTest.kt b/experimental/behave/src/smoke-test/kotlin/net/corda/behave/service/proxy/OSNetworkTest.kt new file mode 100644 index 0000000000..54ddaab82f --- /dev/null +++ b/experimental/behave/src/smoke-test/kotlin/net/corda/behave/service/proxy/OSNetworkTest.kt @@ -0,0 +1,34 @@ +package net.corda.behave.service.proxy + +import net.corda.behave.network.Network +import net.corda.behave.node.configuration.NotaryType +import org.junit.AfterClass +import org.junit.BeforeClass + +class OSNetworkTest : CordaRPCProxyClientTest() { + + /** + * Ensure you have configured the environment correctly by running the "prepare.sh" script + * and then use a JVM option to ensure that STAGING_ROOT points to corresponding location used in the above script. + * eg. + * -ea -DSTAGING_ROOT=/home/staging + * + * Use -DDISABLE_CLEANUP=true to prevent deletion of the run-time configuration directories. + */ + + companion object { + private lateinit var network : Network + + @BeforeClass + @JvmStatic fun setUp() { + network = Network.new().addNode(name = "Notary", notaryType = NotaryType.NON_VALIDATING, withRPCProxy = true).generate() + network.start() + network.waitUntilRunning() + } + + @AfterClass + @JvmStatic fun tearDown() { + network.stop() + } + } +} \ No newline at end of file diff --git a/experimental/behave/src/smoke-test/kotlin/net/corda/behave/service/proxy/R3CordaNetworkTest.kt b/experimental/behave/src/smoke-test/kotlin/net/corda/behave/service/proxy/R3CordaNetworkTest.kt new file mode 100644 index 0000000000..f314348ea2 --- /dev/null +++ b/experimental/behave/src/smoke-test/kotlin/net/corda/behave/service/proxy/R3CordaNetworkTest.kt @@ -0,0 +1,36 @@ +package net.corda.behave.service.proxy + +import net.corda.behave.network.Network +import net.corda.behave.node.Distribution.Companion.R3_MASTER +import net.corda.behave.node.Distribution.Type.R3_CORDA +import net.corda.behave.node.configuration.NotaryType +import org.junit.AfterClass +import org.junit.BeforeClass + +class R3CordaNetworkTest : CordaRPCProxyClientTest() { + + /** + * Ensure you have configured the environment correctly by running the "prepare.sh" script + * and then use a JVM option to ensure that STAGING_ROOT points to corresponding location used in the above script. + * eg. + * -ea -DSTAGING_ROOT=/home/staging + * + * Use -DDISABLE_CLEANUP=true to prevent deletion of the run-time configuration directories. + */ + + companion object { + private lateinit var network : Network + + @BeforeClass + @JvmStatic fun setUp() { + network = Network.new(R3_CORDA).addNode(distribution = R3_MASTER, name = "Notary", notaryType = NotaryType.NON_VALIDATING, withRPCProxy = true).generate() + network.start() + network.waitUntilRunning() + } + + @AfterClass + @JvmStatic fun tearDown() { + network.stop() + } + } +} \ No newline at end of file diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt index 38497cc0f5..113c3489a7 100644 --- a/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt +++ b/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt @@ -11,13 +11,79 @@ package net.corda.behave.network import net.corda.behave.database.DatabaseType +import net.corda.behave.node.Distribution import net.corda.behave.node.configuration.NotaryType +import net.corda.core.utilities.hours import net.corda.core.utilities.seconds import org.junit.Ignore import org.junit.Test +import java.time.Duration class NetworkTests { + @Ignore + @Test + fun `OS Corda network of single node with RPC proxy can be spun up`() { + val distribution = Distribution.MASTER + val network = Network + .new() + .addNode(name = "Foo", distribution = distribution, notaryType = NotaryType.NON_VALIDATING, withRPCProxy = true) + .generate() + network.use { + it.waitUntilRunning(300.seconds) + it.keepAlive(300.seconds) + it.signal() + } + } + + @Ignore + @Test + fun `R3 Corda network of single node with RPC proxy can be spun up`() { + val network = Network + .new(Distribution.Type.R3_CORDA) + .addNode(name = "Notary", distribution = Distribution.R3_MASTER, notaryType = NotaryType.NON_VALIDATING, withRPCProxy = true) + .generate() + network.use { + it.waitUntilRunning(1.hours) + it.keepAlive(1.hours) + it.signal() + } + } + + @Ignore + @Test + fun `Mixed OS Corda network of two nodes (with an RPC proxy each) and a non-validating notary can be spun up`() { + // Note: this test exercises the NetworkBootstrapper to setup a local network + val distribution = Distribution.MASTER + val network = Network + .new() + .addNode(name = "EntityA", distribution = Distribution.MASTER, withRPCProxy = true) + .addNode(name = "EntityB", distribution = Distribution.R3_MASTER, withRPCProxy = true) + .addNode(name = "Notary", distribution = distribution, notaryType = NotaryType.NON_VALIDATING) + .generate() + network.use { + it.waitUntilRunning(Duration.ofDays(1)) + it.keepAlive(Duration.ofDays(1)) + } + } + + @Ignore + @Test + fun `Mixed R3 Corda network of two nodes (with an RPC proxy each) and a non-validating notary can be spun up`() { + // Note: this test exercises the Doorman / Notary / NMS setup sequence + val distribution = Distribution.R3_MASTER + val network = Network + .new() + .addNode(name = "EntityA", distribution = Distribution.R3_MASTER, withRPCProxy = true) + .addNode(name = "EntityB", distribution = Distribution.MASTER, withRPCProxy = true) + .addNode(name = "Notary", distribution = distribution, notaryType = NotaryType.NON_VALIDATING) + .generate() + network.use { + it.waitUntilRunning(Duration.ofDays(1)) + it.keepAlive(Duration.ofDays(1)) + } + } + @Ignore @Test fun `network of two nodes can be spun up`() { diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt index 55f565f817..88161f4f07 100644 --- a/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt +++ b/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt @@ -31,7 +31,7 @@ class CommandTests { @Test fun `output stream for command can be observed`() { val subscriber = TestSubscriber() - val exitCode = Command(listOf("ls", "/")).use(subscriber) { _, output -> + val exitCode = Command(listOf("ls", "/")).use(subscriber) { _, _ -> subscriber.awaitTerminalEvent() subscriber.assertCompleted() subscriber.assertNoErrors() diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionOracleImpl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionOracleImpl.kt index 24d3bc9c86..bf9e6170b4 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionOracleImpl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionOracleImpl.kt @@ -23,15 +23,15 @@ import java.util.* class CashSelectionOracleImpl : AbstractCashSelection(maxRetries = 16, retrySleep = 1000, retryCap = 5000) { companion object { - val JDBC_DRIVER_NAME = "Oracle JDBC driver" + const val JDBC_DRIVER_NAME = "Oracle JDBC driver" private val log = contextLogger() } override fun isCompatible(metaData: DatabaseMetaData): Boolean { - return metaData.driverName == JDBC_DRIVER_NAME + return metaData.driverName.startsWith(JDBC_DRIVER_NAME, ignoreCase = true) } - override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME" + override fun toString() = "${this::class.qualifiedName} for '$JDBC_DRIVER_NAME'" override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, withResultSet: (ResultSet) -> Boolean): Boolean { diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt index d219fb3a2e..33053ddabf 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt @@ -28,7 +28,7 @@ class CashSelectionPostgreSQLImpl : AbstractCashSelection() { } override fun isCompatible(metadata: DatabaseMetaData): Boolean { - return metadata.driverName == JDBC_DRIVER_NAME + return metadata.driverName.startsWith(JDBC_DRIVER_NAME, ignoreCase = true) } override fun toString() = "${this::class.qualifiedName} for '$JDBC_DRIVER_NAME'" diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt index bd9f6c16a7..20d0c5343b 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt @@ -28,15 +28,15 @@ import java.util.* class CashSelectionSQLServerImpl : AbstractCashSelection(maxRetries = 16, retrySleep = 1000, retryCap = 5000) { companion object { - val JDBC_DRIVER_NAME = "Microsoft JDBC Driver 6.2 for SQL Server" + const val JDBC_DRIVER_NAME = "Microsoft JDBC Driver" private val log = contextLogger() } override fun isCompatible(metaData: DatabaseMetaData): Boolean { - return metaData.driverName == JDBC_DRIVER_NAME + return metaData.driverName.startsWith(JDBC_DRIVER_NAME, ignoreCase = true) } - override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME" + override fun toString() = "${this::class.qualifiedName} for '$JDBC_DRIVER_NAME'" override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 661fe4976e..80de618169 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -32,11 +32,24 @@ sourceSets { srcDir file('../testing/test-utils/src/main/resources') } } + scenarioTest { + kotlin { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/system-test/scenario/kotlin') + } + resources { + srcDir 'src/system-test/scenario/resources' + } + } } configurations { integrationTestCompile.extendsFrom testCompile integrationTestRuntime.extendsFrom testRuntime + + scenarioTestCompile.extendsFrom testCompile + scenarioTestRuntime.extendsFrom testRuntime } dependencies { @@ -69,6 +82,7 @@ dependencies { // Test dependencies testCompile project(':node-driver') + scenarioTestCompile project(path: ":experimental:behave", configuration: 'testArtifacts') testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:${assertj_version}" } @@ -147,6 +161,16 @@ task integrationTest(type: Test, dependsOn: []) { classpath = sourceSets.integrationTest.runtimeClasspath } +task scenarioTest(type: Test, dependsOn: []) { + testClassesDirs = sourceSets.scenarioTest.output.classesDirs + classpath = sourceSets.scenarioTest.runtimeClasspath +} + +task scenarioJar(type: Jar, dependsOn: classes) { + classifier "behave-test" + from sourceSets.scenarioTest.output +} + publishing { publications { simmvaluationdemo(MavenPublication) { @@ -155,6 +179,7 @@ publishing { artifact sourceJar artifact javadocJar + artifact scenarioJar } } } diff --git a/samples/simm-valuation-demo/src/system-test/scenario/kotlin/net/corda/vega/scenarios/SIMMValuationStepsProvider.kt b/samples/simm-valuation-demo/src/system-test/scenario/kotlin/net/corda/vega/scenarios/SIMMValuationStepsProvider.kt new file mode 100644 index 0000000000..6448d9aeb1 --- /dev/null +++ b/samples/simm-valuation-demo/src/system-test/scenario/kotlin/net/corda/vega/scenarios/SIMMValuationStepsProvider.kt @@ -0,0 +1,14 @@ +package net.corda.vega.scenarios + +import net.corda.behave.scenarios.api.StepsBlock +import net.corda.behave.scenarios.api.StepsProvider +import net.corda.vega.scenarios.steps.SimmValuationSteps + +class SIMMValuationStepsProvider : StepsProvider { + + override val name: String + get() = SIMMValuationStepsProvider::javaClass.name + + override val stepsDefinition: StepsBlock + get() = SimmValuationSteps() +} diff --git a/samples/simm-valuation-demo/src/system-test/scenario/kotlin/net/corda/vega/scenarios/helpers/SIMMValuation.kt b/samples/simm-valuation-demo/src/system-test/scenario/kotlin/net/corda/vega/scenarios/helpers/SIMMValuation.kt new file mode 100644 index 0000000000..15f580da5e --- /dev/null +++ b/samples/simm-valuation-demo/src/system-test/scenario/kotlin/net/corda/vega/scenarios/helpers/SIMMValuation.kt @@ -0,0 +1,43 @@ +package net.corda.vega.scenarios.helpers + +import com.opengamma.strata.product.common.BuySell +import net.corda.behave.scenarios.ScenarioState +import net.corda.behave.scenarios.helpers.Substeps +import net.corda.core.messaging.startFlow +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.getOrThrow +import net.corda.vega.api.SwapDataModel +import net.corda.vega.flows.IRSTradeFlow +import java.math.BigDecimal +import java.time.LocalDate + +class SIMMValuation(state: ScenarioState) : Substeps(state) { + + private companion object { + // SIMM demo can only currently handle one valuation date due to a lack of market data or a market data source. + val valuationDate: LocalDate = LocalDate.parse("2016-06-06") + val tradeId = "trade1" + } + + fun trade(ownNode: String, counterpartyNode: String) : SignedTransaction { + return withClientProxy(ownNode) { + val swap = SwapDataModel(tradeId, "desc", valuationDate, "EUR_FIXED_1Y_EURIBOR_3M", + valuationDate, LocalDate.parse("2020-01-02"), BuySell.BUY, BigDecimal.valueOf(1000), BigDecimal.valueOf(0.1)) + + val ownParty = it.partiesFromName(ownNode, false).first() + val counterParty = it.partiesFromName(counterpartyNode, false).first() + + val buyer = if (swap.buySell.isBuy) ownParty else counterParty + val seller = if (swap.buySell.isSell) ownParty else counterParty + return@withClientProxy it.startFlow(IRSTradeFlow::Requester, swap.toData(buyer, seller), ownParty).returnValue.getOrThrow() + } + } + + fun runValuation(node: String) { + TODO("not implemented") + } + + fun checkValuation(value: Long) { + TODO("not implemented") + } +} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/system-test/scenario/kotlin/net/corda/vega/scenarios/steps/SimmValuationSteps.kt b/samples/simm-valuation-demo/src/system-test/scenario/kotlin/net/corda/vega/scenarios/steps/SimmValuationSteps.kt new file mode 100644 index 0000000000..424c044e82 --- /dev/null +++ b/samples/simm-valuation-demo/src/system-test/scenario/kotlin/net/corda/vega/scenarios/steps/SimmValuationSteps.kt @@ -0,0 +1,30 @@ +package net.corda.vega.scenarios.steps + +import net.corda.behave.scenarios.ScenarioState +import net.corda.behave.scenarios.api.StepsBlock +import net.corda.vega.scenarios.helpers.SIMMValuation + +class SimmValuationSteps : StepsBlock { + + override fun initialize(state: ScenarioState) { + val simmValuation = SIMMValuation(state) + + Then("^node (\\w+) can trade with node (\\w+)$") { nodeA, nodeB -> + state.withNetwork { + simmValuation.trade(nodeA, nodeB) + } + } + + Then("^node (\\w+) can run portfolio valuation$") { node -> + state.withNetwork { + simmValuation.runValuation(node) + } + } + + Then("^node (\\w+) portfolio valuation is (\\d+)$") { _, value -> + state.withNetwork { + simmValuation.checkValuation(value) + } + } + } +} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/system-test/scenario/resources/features/simm-valuation.feature b/samples/simm-valuation-demo/src/system-test/scenario/resources/features/simm-valuation.feature new file mode 100644 index 0000000000..0bc22ea8e3 --- /dev/null +++ b/samples/simm-valuation-demo/src/system-test/scenario/resources/features/simm-valuation.feature @@ -0,0 +1,63 @@ +@qa compatibility @node @cordapps +Feature: Compatibility - Corda distributions (OS and Enterprise) running different CorDapps + To support an interoperable Corda network, different CorDapps must have the ability to transact in mixed Corda (OS) and R3 Corda (Enterprise) networks. + + Scenario Outline: Run the SIMM valuation demo in a Corda OS Network. + Given a node PartyA of version with proxy + And node PartyA has app installed: + And a node PartyB of version + And node PartyB has app installed: + And a nonvalidating notary Notary of version + When the network is ready + And node PartyA has loaded app + And node PartyB has loaded app + Then node PartyA can trade with node PartyB + And node PartyA vault contains 1 states + And node PartyB vault contains 1 states + And node PartyA can run portfolio valuation + And node PartyA portfolio valuation is + And node PartyB portfolio valuation is + + Examples: + | Corda-Node-Version | Cordapp-Name | Valuation | + | corda-3.0 | simm-valuation-demo | 12345 | + + Scenario Outline: Run the SIMM valuation demo in an R3 Corda Network. + Given a node PartyA of version with proxy + And node PartyA has app installed: + And a node PartyB of version + And node PartyB has app installed: + And a nonvalidating notary Notary of version + When the network is ready + And node PartyA has loaded app + And node PartyB has loaded app + Then node PartyA can trade with node PartyB + And node PartyA vault contains 1 states + And node PartyB vault contains 1 states + And node PartyA can run portfolio valuation + And node PartyA portfolio valuation is + And node PartyB portfolio valuation is + + Examples: + | R3-Corda-Node-Version | Cordapp-Name | Valuation | + | R3.CORDA-3.0.0-DEV-PREVIEW-3 | simm-valuation-demo | 12345 | + + Scenario Outline: Corda (OS) Node can transact with R3 Corda (Enterprise) node using the SIMM valuation demo. + Given a node PartyA of version with proxy + And node PartyA has app installed: + And a node PartyB of version + And node PartyB has app installed: + And a nonvalidating notary Notary of version + When the network is ready + And node PartyA has loaded app + And node PartyB has loaded app + Then node PartyA can trade with node PartyB + And node PartyA vault contains 1 states + And node PartyB vault contains 1 states + And node PartyA can run portfolio valuation + And node PartyA portfolio valuation is + And node PartyB portfolio valuation is + + Examples: + | Corda-Node-Version | R3-Corda-Node-Version | Cordapp-Name | Valuation | + | corda-3.0 | R3.CORDA-3.0.0-DEV-PREVIEW-3 | simm-valuation-demo | 12345 | diff --git a/samples/simm-valuation-demo/src/system-test/scenario/resources/scripts/run-behave-simm-valuation.sh b/samples/simm-valuation-demo/src/system-test/scenario/resources/scripts/run-behave-simm-valuation.sh new file mode 100755 index 0000000000..6663b57517 --- /dev/null +++ b/samples/simm-valuation-demo/src/system-test/scenario/resources/scripts/run-behave-simm-valuation.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# +# Run this script from the sample directory +# +# $ pwd +# ./IdeaProjects/corda-reviews/samples/simm-valuation-demo +# $ src/system-test/scenario/resources/scripts/run-behave-simm-valuation.sh +# +# + +CURRENT_DIR=$PWD + +BEHAVE_DIR=$CURRENT_DIR/../../experimental/behave +cd $BEHAVE_DIR +../../gradlew behaveJar + +DEMO_DIR=$CURRENT_DIR +cd $DEMO_DIR +../../gradlew scenarioJar + +echo $BEHAVE_DIR +java -cp "$BEHAVE_DIR/build/libs/corda-behave.jar:$CURRENT_DIR/build/libs/simm-valuation-demo-behave-test.jar" net.corda.behave.scenarios.ScenarioRunner --glue "net.corda.behave.scenarios" -path ./src/system-test/scenario/resources/features/simm-valuation.feature -d +# -d to perform dry-run \ No newline at end of file diff --git a/testing/qa/behave/compatibility/resources/scripts/run-interoperability.sh b/testing/qa/behave/compatibility/resources/scripts/run-interoperability.sh index 2ea4d4490f..1ce4643ad1 100755 --- a/testing/qa/behave/compatibility/resources/scripts/run-interoperability.sh +++ b/testing/qa/behave/compatibility/resources/scripts/run-interoperability.sh @@ -13,4 +13,4 @@ cd ${BEHAVE_DIR} ../../gradlew behaveJar # QA interoperability -java -jar ${BEHAVE_DIR}/build/libs/corda-behave.jar -d --glue net.corda.behave.scenarios -path ${R3CORDA_HOME}/testing/qa/behave/compatibility/resources/features/qa-interop-testing.feature +java -jar ${BEHAVE_DIR}/build/libs/corda-behave.jar -d --glue net.corda.behave.scenarios -path ${R3CORDA_HOME}/testing/qa/behave/compatibility/resources/features/interoperability.feature diff --git a/testing/qa/behave/functional/resources/scripts/run-functional.sh b/testing/qa/behave/functional/resources/scripts/run-functional.sh index a0c2c45a4a..4853c28a43 100755 --- a/testing/qa/behave/functional/resources/scripts/run-functional.sh +++ b/testing/qa/behave/functional/resources/scripts/run-functional.sh @@ -14,4 +14,4 @@ cd ${BEHAVE_DIR} ../../gradlew behaveJar # QA functional -java -jar ${BEHAVE_DIR}/build/libs/corda-behave.jar -d --glue net.corda.behave.scenarios -path ${R3CORDA_HOME}/testing/qa/behave/functional/resources/features/qa-functional-testing.feature +java -jar ${BEHAVE_DIR}/build/libs/corda-behave.jar -d --glue net.corda.behave.scenarios -path ${R3CORDA_HOME}/testing/qa/behave/functional/resources/features/functional.feature