diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index 53ac140c61..183b276fb8 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -129,3 +129,8 @@ fun Future.getOrThrow(timeout: Duration? = null): V = try { } catch (e: ExecutionException) { throw e.cause!! } + +/** + * Extension method for providing a sumBy method that processes and returns a Long + */ +fun Iterable.sumByLong(selector: (T) -> Long): Long = this.map { selector(it) }.sum() diff --git a/experimental/behave/README.md b/experimental/behave/README.md index b3b37ca2e9..9f5d32a2d2 100644 --- a/experimental/behave/README.md +++ b/experimental/behave/README.md @@ -5,7 +5,7 @@ and test homogeneous and heterogeneous Corda networks on a local machine. The framework has built-in support for Dockerised node dependencies so that you easily can spin up a Corda node locally that, for instance, uses a 3rd party database provider such as -MS SQL Server or Postgres. +Postgres. # Structure diff --git a/experimental/behave/build.gradle b/experimental/behave/build.gradle index 4148d9d462..11533f5fdc 100644 --- a/experimental/behave/build.gradle +++ b/experimental/behave/build.gradle @@ -66,6 +66,12 @@ dependencies { compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" compile "org.apache.logging.log4j:log4j-core:$log4j_version" + // JOptSimple: command line option parsing + compile "net.sf.jopt-simple:jopt-simple:$jopt_simple_version" + + // FastClasspathScanner: classpath scanning + compile 'io.github.lukehutch:fast-classpath-scanner:2.12.3' + compile "commons-io:commons-io:$commonsio_version" compile "commons-logging:commons-logging:$commonslogging_version" compile "com.spotify:docker-client:$docker_client_version" @@ -85,7 +91,6 @@ dependencies { scenarioCompile "info.cukes:cucumber-java8:$cucumber_version" scenarioCompile "info.cukes:cucumber-junit:$cucumber_version" scenarioCompile "info.cukes:cucumber-picocontainer:$cucumber_version" - } compileKotlin { diff --git a/experimental/behave/deps/corda/3.0.0/.gitkeep b/experimental/behave/deps/corda/3.0.0/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/experimental/behave/deps/corda/3.0.0/apps/.gitkeep b/experimental/behave/deps/corda/3.0.0/apps/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/experimental/behave/deps/drivers/README.md b/experimental/behave/deps/drivers/README.md index 19ff783c5f..6ca2db42d6 100644 --- a/experimental/behave/deps/drivers/README.md +++ b/experimental/behave/deps/drivers/README.md @@ -1,3 +1,4 @@ Download and store database drivers here; for example: - h2-1.4.196.jar - - mssql-jdbc-6.2.2.jre8.jar + - postgresql-42.1.4.jar + \ No newline at end of file diff --git a/experimental/behave/prepare.sh b/experimental/behave/prepare.sh index 02c5df162b..36a3515137 100755 --- a/experimental/behave/prepare.sh +++ b/experimental/behave/prepare.sh @@ -1,24 +1,31 @@ #!/bin/bash -VERSION=3.0.0 +set -x + +# Please ensure you run this script using source code (eg. GitHub master, branch or TAG) that reflects the version label defined below +# For example: +# corda-master => git clone https://github.com/corda/corda +# r3corda-master => git clone https://github.com/corda/enterprise +VERSION=corda-3.0 +STAGING_DIR=deps/corda/${VERSION} +DRIVERS_DIR=deps/drivers # Set up directories -mkdir -p deps/corda/${VERSION}/apps -mkdir -p deps/drivers +mkdir -p ${STAGING_DIR}/apps +mkdir -p ${DRIVERS_DIR} # Copy Corda capsule into deps -cp -v $(ls ../../node/capsule/build/libs/corda-*.jar | tail -n1) deps/corda/${VERSION}/corda.jar +cd ../.. +./gradlew clean :node:capsule:buildCordaJar :finance:jar +cp -v $(ls node/capsule/build/libs/corda-*.jar | tail -n1) experimental/behave/${STAGING_DIR}/corda.jar + +# Copy finance library +cp -v $(ls finance/build/libs/corda-finance-*.jar | tail -n1) experimental/behave/${STAGING_DIR}/apps # Download database drivers -curl "https://search.maven.org/remotecontent?filepath=com/h2database/h2/1.4.196/h2-1.4.196.jar" > deps/drivers/h2-1.4.196.jar -curl -L "https://github.com/Microsoft/mssql-jdbc/releases/download/v6.2.2/mssql-jdbc-6.2.2.jre8.jar" > deps/drivers/mssql-jdbc-6.2.2.jre8.jar +curl "https://search.maven.org/remotecontent?filepath=com/h2database/h2/1.4.196/h2-1.4.196.jar" > experimental/behave/${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" > experimental/behave/${DRIVERS_DIR}/postgresql-42.1.4.jar -# Build required artefacts -cd ../.. +# Build Network Bootstrapper ./gradlew buildBootstrapperJar -./gradlew :finance:jar - -# Copy build artefacts into deps -cd experimental/behave -cp -v $(ls ../../tools/bootstrapper/build/libs/*.jar | tail -n1) deps/corda/${VERSION}/network-bootstrapper.jar -cp -v $(ls ../../finance/build/libs/corda-finance-*.jar | tail -n1) deps/corda/${VERSION}/apps/corda-finance.jar +cp -v $(ls tools/bootstrapper/build/libs/*.jar | tail -n1) experimental/behave/${STAGING_DIR}/network-bootstrapper.jar diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseSettings.kt b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseSettings.kt index 4e870f77de..f0cb2074e4 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseSettings.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseSettings.kt @@ -16,6 +16,9 @@ class DatabaseSettings { var userName: String = "sa" private set + var driverJar: String? = null + private set + private var databaseConfigTemplate: DatabaseConfigurationTemplate = DatabaseConfigurationTemplate() private val serviceInitiators = mutableListOf() @@ -30,6 +33,11 @@ class DatabaseSettings { return this } + fun withDriver(name: String): DatabaseSettings { + driverJar = name + return this + } + fun withUser(name: String): DatabaseSettings { userName = name return this 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 851a8d1387..dad351596c 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 @@ -1,11 +1,11 @@ package net.corda.behave.database import net.corda.behave.database.configuration.H2ConfigurationTemplate -import net.corda.behave.database.configuration.SqlServerConfigurationTemplate +import net.corda.behave.database.configuration.PostgresConfigurationTemplate import net.corda.behave.node.configuration.Configuration import net.corda.behave.node.configuration.DatabaseConfiguration import net.corda.behave.service.database.H2Service -import net.corda.behave.service.database.SqlServerService +import net.corda.behave.service.database.PostgreSQLService enum class DatabaseType(val settings: DatabaseSettings) { @@ -19,16 +19,19 @@ enum class DatabaseType(val settings: DatabaseSettings) { } ), - SQL_SERVER(DatabaseSettings() - .withDatabase(SqlServerService.database) - .withSchema(SqlServerService.schema) - .withUser(SqlServerService.username) - .withConfigTemplate(SqlServerConfigurationTemplate()) + POSTGRES(DatabaseSettings() + .withDatabase(PostgreSQLService.database) + .withDriver(PostgreSQLService.driver) + .withSchema(PostgreSQLService.schema) + .withUser(PostgreSQLService.username) + .withConfigTemplate(PostgresConfigurationTemplate()) .withServiceInitiator { - SqlServerService("sqlserver-${it.name}", it.database.port, it.database.password) + PostgreSQLService("postgres-${it.name}", it.database.port, it.database.password) } ); + val driverJar = settings.driverJar + fun dependencies(config: Configuration) = settings.dependencies(config) fun connection(config: DatabaseConfiguration) = DatabaseConnection(config, settings.template) @@ -37,9 +40,8 @@ enum class DatabaseType(val settings: DatabaseSettings) { fun fromName(name: String): DatabaseType? = when (name.toLowerCase()) { "h2" -> H2 - "sql_server" -> SQL_SERVER - "sql server" -> SQL_SERVER - "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/PostgresConfigurationTemplate.kt similarity index 67% rename from experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/SqlServerConfigurationTemplate.kt rename to experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/PostgresConfigurationTemplate.kt index 370a241d20..e521bd1ee2 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/SqlServerConfigurationTemplate.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/PostgresConfigurationTemplate.kt @@ -3,26 +3,23 @@ package net.corda.behave.database.configuration import net.corda.behave.database.DatabaseConfigurationTemplate import net.corda.behave.node.configuration.DatabaseConfiguration -class SqlServerConfigurationTemplate : DatabaseConfigurationTemplate() { +class PostgresConfigurationTemplate : DatabaseConfigurationTemplate() { override val connectionString: (DatabaseConfiguration) -> String - get() = { "jdbc:sqlserver://${it.host}:${it.port};database=${it.database}" } + get() = { "jdbc:postgresql://${it.host}:${it.port}/${it.database}" } override val config: (DatabaseConfiguration) -> String get() = { """ |dataSourceProperties = { - | dataSourceClassName = "com.microsoft.sqlserver.jdbc.SQLServerDataSource" + | dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource" | 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 405404fcb2..621994a5ca 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 @@ -5,4 +5,10 @@ import java.io.File val currentDirectory: File get() = File(System.getProperty("user.dir")) +// location of Corda distributions and Drivers dependencies +val stagingRoot: File + get() = if (System.getProperty("STAGING_ROOT") != null) + File(System.getProperty("STAGING_ROOT")) + else currentDirectory + operator fun File.div(relative: String): File = this.resolve(relative) diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/ConjunctiveWatch.kt b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/ConjunctiveWatch.kt index c0308fca1e..0b65fc338e 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/ConjunctiveWatch.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/ConjunctiveWatch.kt @@ -1,23 +1,24 @@ package net.corda.behave.monitoring import net.corda.behave.await -import rx.Observable import java.time.Duration import java.util.concurrent.CountDownLatch class ConjunctiveWatch( private val left: Watch, private val right: Watch -) : Watch() { +) : Watch { - override fun await(observable: Observable, timeout: Duration): Boolean { - val latch = CountDownLatch(2) + override fun ready() = left.ready() && right.ready() + + override fun await(timeout: Duration): Boolean { + val countDownLatch = CountDownLatch(2) listOf(left, right).parallelStream().forEach { - if (it.await(observable, timeout)) { - latch.countDown() + if (it.await(timeout)) { + countDownLatch.countDown() } } - return latch.await(timeout) + return countDownLatch.await(timeout) } } \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/DisjunctiveWatch.kt b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/DisjunctiveWatch.kt index 061ca1ed61..444d47ddb7 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/DisjunctiveWatch.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/DisjunctiveWatch.kt @@ -8,16 +8,18 @@ import java.util.concurrent.CountDownLatch class DisjunctiveWatch( private val left: Watch, private val right: Watch -) : Watch() { +) : Watch { - override fun await(observable: Observable, timeout: Duration): Boolean { - val latch = CountDownLatch(1) + override fun ready() = left.ready() || right.ready() + + override fun await(timeout: Duration): Boolean { + val countDownLatch = CountDownLatch(1) listOf(left, right).parallelStream().forEach { - if (it.await(observable, timeout)) { - latch.countDown() + if (it.await(timeout)) { + countDownLatch.countDown() } } - return latch.await(timeout) + return countDownLatch.await(timeout) } } diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/PatternWatch.kt b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/PatternWatch.kt index 50a715dd79..bd62ee786e 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/PatternWatch.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/PatternWatch.kt @@ -1,22 +1,24 @@ package net.corda.behave.monitoring +import rx.Observable + class PatternWatch( + observable: Observable, pattern: String, ignoreCase: Boolean = false -) : Watch() { +) : AbstractWatch(observable, false) { - private val regularExpression = if (ignoreCase) { + private val regularExpression: Regex = if (ignoreCase) { Regex("^.*$pattern.*$", RegexOption.IGNORE_CASE) } else { Regex("^.*$pattern.*$") } - override fun match(data: String) = regularExpression.matches(data.trim()) - - companion object { - - val EMPTY = PatternWatch("") - + init { + run() } + override fun match(data: String): Boolean { + return regularExpression.matches(data.trim()) + } } \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/Watch.kt b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/Watch.kt index c5b7d94920..ee835ac425 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/Watch.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/Watch.kt @@ -6,28 +6,46 @@ import rx.Observable import java.time.Duration import java.util.concurrent.CountDownLatch -abstract class Watch { - - private val latch = CountDownLatch(1) - - open fun await( - observable: Observable, - timeout: Duration = 10.seconds - ): Boolean { - observable - .filter { match(it) } - .forEach { latch.countDown() } - return latch.await(timeout) - } - - open fun match(data: String): Boolean = false +interface Watch { + fun await(timeout: Duration = 10.seconds): Boolean + fun ready(): Boolean operator fun times(other: Watch): Watch { return ConjunctiveWatch(this, other) } - operator fun div(other: Watch): Watch { return DisjunctiveWatch(this, other) } +} +/** + * @param [observable] refers to an observable stream of events + * @param [autostart] is true starting of Watch can be deferred - it helps in case of initialization + * order problems (like match()) using fields from subclass which won't get initialized before superclass + * constructor finishes. It is the responsibility of the subclass to manually call the run method + * if autostart is false. + */ +abstract class AbstractWatch(val observable: Observable, autostart: Boolean = true) : Watch { + + private val latch = CountDownLatch(1) + + init { + if (autostart) { + run() + } + } + + fun run() { + observable.exists { match(it) }.filter { it }.subscribe { + latch.countDown() + } + } + + override fun await(timeout: Duration): Boolean { + return latch.await(timeout) + } + + override fun ready(): Boolean = latch.count == 0L + + open fun match(data: T): Boolean = false } \ 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 d0cabfc02c..9da2bfb63d 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 @@ -4,12 +4,14 @@ import net.corda.behave.database.DatabaseType import net.corda.behave.file.LogSource import net.corda.behave.file.currentDirectory import net.corda.behave.file.div +import net.corda.behave.file.stagingRoot import net.corda.behave.logging.getLogger import net.corda.behave.minutes import net.corda.behave.node.Distribution import net.corda.behave.node.Node import net.corda.behave.node.configuration.NotaryType import net.corda.behave.process.JarCommand +import net.corda.core.CordaException import org.apache.commons.io.FileUtils import java.io.Closeable import java.io.File @@ -26,8 +28,6 @@ class Network private constructor( private val timeout: Duration = 2.minutes ) : Closeable, Iterable { - private val log = getLogger() - private val latch = CountDownLatch(1) private var isRunning = false @@ -36,6 +36,10 @@ class Network private constructor( private var hasError = false + init { + FileUtils.forceMkdir(targetDirectory) + } + class Builder internal constructor( private val timeout: Duration ) { @@ -43,7 +47,7 @@ class Network private constructor( private val nodes = mutableMapOf() private val startTime = DateTimeFormatter - .ofPattern("yyyyMMDD-HHmmss") + .ofPattern("yyyyMMdd-HHmmss") .withZone(ZoneId.of("UTC")) .format(Instant.now()) @@ -51,7 +55,7 @@ class Network private constructor( fun addNode( name: String, - distribution: Distribution = Distribution.LATEST_MASTER, + distribution: Distribution = Distribution.MASTER, databaseType: DatabaseType = DatabaseType.H2, notaryType: NotaryType = NotaryType.NONE, issuableCurrencies: List = emptyList() @@ -76,22 +80,28 @@ class Network private constructor( fun generate(): Network { val network = Network(nodes, directory, timeout) - network.bootstrapNetwork() + + network.copyDatabaseDrivers() + if (!network.configureNodes()) { + throw CordaException("Unable to configure nodes in Corda network. Please check logs in $directory") + } + network.bootstrapLocalNetwork() + return network } - } - private fun copyDatabaseDrivers() { + fun copyDatabaseDrivers() { val driverDirectory = targetDirectory / "libs" + log.info("Copying database drivers from $stagingRoot/deps/drivers to $driverDirectory") FileUtils.forceMkdir(driverDirectory) FileUtils.copyDirectory( - currentDirectory / "deps/drivers", + stagingRoot / "deps/drivers", driverDirectory ) } - private fun configureNodes(): Boolean { + fun configureNodes(): Boolean { var allDependenciesStarted = true log.info("Configuring nodes ...") for (node in nodes.values) { @@ -109,19 +119,14 @@ class Network private constructor( } } - private fun bootstrapNetwork() { - copyDatabaseDrivers() - if (!configureNodes()) { - hasError = true - return - } + private fun bootstrapLocalNetwork() { val bootstrapper = nodes.values .sortedByDescending { it.config.distribution.version } .first() .config.distribution.networkBootstrapper if (!bootstrapper.exists()) { - log.warn("Network bootstrapping tool does not exist; continuing ...") + signalFailure("Network bootstrapping tool does not exist; exiting ...") return } @@ -205,11 +210,15 @@ class Network private constructor( } isRunning = true for (node in nodes.values) { + log.info("Starting node [{}]", node.config.name) node.start() } } fun waitUntilRunning(waitDuration: Duration? = null): Boolean { + + log.info("Network.waitUntilRunning") + if (hasError) { return false } @@ -264,6 +273,7 @@ class Network private constructor( } log.info("Shutting down network ...") isStopped = true + log.info("Shutting down nodes ...") for (node in nodes.values) { node.shutDown() } @@ -289,13 +299,10 @@ class Network private constructor( } companion object { - + val log = getLogger() const val CLEANUP_ON_ERROR = false - fun new( - timeout: Duration = 2.minutes + fun new(timeout: Duration = 2.minutes ): Builder = Builder(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 cca3570f39..da82242968 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 @@ -1,6 +1,9 @@ package net.corda.behave.node import net.corda.behave.file.div +import net.corda.behave.file.stagingRoot +import net.corda.behave.logging.getLogger +import net.corda.behave.service.Service import org.apache.commons.io.FileUtils import java.io.File import java.net.URL @@ -23,39 +26,53 @@ class Distribution private constructor( /** * The URL of the distribution fat JAR, if available. */ - val url: URL? = null + val url: URL? = null, + /** + * The Docker image details, if available + */ + val baseImage: String? = null ) { /** * The path to the distribution fat JAR. */ - val jarFile: File = file ?: nodePrefix / "$version/corda.jar" + val path: File = file ?: nodePrefix / "$version" + + /** + * The path to the distribution fat JAR. + */ + val cordaJar: File = path / "corda.jar" /** * The path to available Cordapps for this distribution. */ - val cordappDirectory: File = nodePrefix / "$version/apps" + val cordappDirectory: File = path / "apps" /** * The path to network bootstrapping tool. */ - val networkBootstrapper: File = nodePrefix / "$version/network-bootstrapper.jar" + val networkBootstrapper: File = path / "network-bootstrapper.jar" /** * Ensure that the distribution is available on disk. */ fun ensureAvailable() { - if (!jarFile.exists()) { + if (!cordaJar.exists()) { if (url != null) { try { - FileUtils.forceMkdirParent(jarFile) - FileUtils.copyURLToFile(url, jarFile) + FileUtils.forceMkdirParent(cordaJar) + FileUtils.copyURLToFile(url, cordaJar) } catch (e: Exception) { - throw Exception("Invalid Corda version $version", e) + if (e.message!!.contains("HTTP response code: 401")) { + log.warn("CORDA_ARTIFACTORY_USERNAME ${System.getenv("CORDA_ARTIFACTORY_USERNAME")}") + log.warn("CORDA_ARTIFACTORY_PASSWORD ${System.getenv("CORDA_ARTIFACTORY_PASSWORD")}") + throw Exception("Incorrect Artifactory permission. Please set CORDA_ARTIFACTORY_USERNAME and CORDA_ARTIFACTORY_PASSWORD environment variables correctly.") + } + else throw Exception("Invalid Corda version $version", e) } } else { - throw Exception("File not found $jarFile") + throw Exception("File not found $cordaJar") } } } @@ -63,29 +80,25 @@ class Distribution private constructor( /** * Human-readable representation of the distribution. */ - override fun toString() = "Corda(version = $version, path = $jarFile)" + override fun toString() = "Corda(version = $version, path = $cordaJar)" companion object { + protected val log = getLogger() + private val distributions = mutableListOf() - private val directory = File(System.getProperty("user.dir")) + private val nodePrefix = stagingRoot / "deps/corda" - private val nodePrefix = directory / "deps/corda" + val MASTER = fromJarFile("corda-master") /** - * Corda Open Source, version 3.0.0 + * Get representation of a Corda distribution from Artifactory based on its version string. + * @param version The version of the Corda distribution. */ - val V3 = fromJarFile("3.0.0") - - val LATEST_MASTER = V3 - - /** - * Get representation of an open source distribution based on its version string. - * @param version The version of the open source Corda distribution. - */ - fun fromOpenSourceVersion(version: String): Distribution { - val url = URL("https://dl.bintray.com/r3/corda/net/corda/corda/$version/corda-$version.jar") + fun fromArtifactory(version: String): Distribution { + val url = URL("https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases/net/corda/corda/$version/corda-$version.jar") + log.info("Artifactory URL: $url\n") val distribution = Distribution(version, url = url) distributions.add(distribution) return distribution @@ -102,15 +115,25 @@ class Distribution private constructor( return distribution } + /** + * Get Corda distribution from a Docker image file. + * @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) + distributions.add(distribution) + return distribution + } + /** * Get registered representation of a Corda distribution based on its version string. * @param version The version of the Corda distribution */ - fun fromVersionString(version: String): Distribution? = when (version.toLowerCase()) { - "master" -> LATEST_MASTER - else -> distributions.firstOrNull { it.version == version } + fun fromVersionString(version: String): Distribution = when (version) { + "master" -> MASTER + "corda-3.0" -> fromArtifactory(version) + else -> fromJarFile(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 7f4b89862b..c768e2b377 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 @@ -5,6 +5,7 @@ import net.corda.behave.database.DatabaseType import net.corda.behave.file.LogSource import net.corda.behave.file.currentDirectory import net.corda.behave.file.div +import net.corda.behave.file.stagingRoot import net.corda.behave.logging.getLogger import net.corda.behave.monitoring.PatternWatch import net.corda.behave.node.configuration.* @@ -20,6 +21,7 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.utilities.NetworkHostAndPort import org.apache.commons.io.FileUtils import java.io.File +import java.net.InetAddress import java.time.Duration import java.util.concurrent.CountDownLatch @@ -39,14 +41,14 @@ class Node( private val logDirectory = runtimeDirectory / "logs" private val command = JarCommand( - config.distribution.jarFile, + config.distribution.cordaJar, arrayOf("--config", "node.conf"), runtimeDirectory, settings.timeout, enableRemoteDebugging = false ) - private val isAliveLatch = PatternWatch("Node for \".*\" started up and registered") + private val isAliveLatch = PatternWatch(command.output, "Node for \".*\" started up and registered") private var isConfigured = false @@ -76,7 +78,7 @@ class Node( log.info("Configuring {} ...", this) serviceDependencies.addAll(config.database.type.dependencies(config)) config.distribution.ensureAvailable() - config.writeToFile(rootDirectory / "${config.name}.conf") + config.writeToFile(rootDirectory / "${config.name}_node.conf") installApps() } @@ -97,7 +99,7 @@ class Node( } fun waitUntilRunning(waitDuration: Duration? = null): Boolean { - val ok = isAliveLatch.await(command.output, waitDuration ?: settings.timeout) + val ok = isAliveLatch.await(waitDuration ?: settings.timeout) if (!ok) { log.warn("{} did not start up as expected within the given time frame", this) } else { @@ -126,7 +128,8 @@ class Node( } val logOutput: LogSource by lazy { - LogSource(logDirectory, "node-info-gen.log", filePatternUsedForExclusion = true) + val hostname = InetAddress.getLocalHost().hostName + LogSource(logDirectory, "node-$hostname.*.log") } val database: DatabaseConnection by lazy { @@ -216,7 +219,7 @@ class Node( private fun installApps() { val version = config.distribution.version - val appDirectory = rootDirectory / "../../../deps/corda/$version/apps" + val appDirectory = stagingRoot / "deps/corda/$version/apps" if (appDirectory.exists()) { val targetAppDirectory = runtimeDirectory / "cordapps" FileUtils.copyDirectory(appDirectory, targetAppDirectory) @@ -228,7 +231,7 @@ class Node( var name: String? = null private set - private var distribution = Distribution.V3 + private var distribution = Distribution.MASTER private var databaseType = DatabaseType.H2 @@ -314,13 +317,14 @@ class Node( databaseType, location = location, country = country, + notary = NotaryConfiguration(notaryType), + cordapps = CordappConfiguration( + apps = apps, + includeFinance = includeFinance + ), configElements = *arrayOf( NotaryConfiguration(notaryType), - CurrencyConfiguration(issuableCurrencies), - CordappConfiguration( - apps = *apps.toTypedArray(), - includeFinance = includeFinance - ) + CurrencyConfiguration(issuableCurrencies) ) ), directory, @@ -331,7 +335,6 @@ class Node( private fun error(message: String): T { throw IllegalArgumentException(message) } - } companion object { 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 ce1cdc44e2..5b625600ee 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 @@ -1,13 +1,15 @@ package net.corda.behave.node.configuration import net.corda.behave.database.DatabaseType +import net.corda.behave.logging.getLogger import net.corda.behave.node.* +import net.corda.core.identity.CordaX500Name import org.apache.commons.io.FileUtils import java.io.File class Configuration( val name: String, - val distribution: Distribution = Distribution.LATEST_MASTER, + val distribution: Distribution = Distribution.MASTER, val databaseType: DatabaseType = DatabaseType.H2, val location: String = "London", val country: String = "GB", @@ -19,19 +21,21 @@ class Configuration( nodeInterface.dbPort, password = DEFAULT_PASSWORD ), + val notary: NotaryConfiguration = NotaryConfiguration(), + val cordapps: CordappConfiguration = CordappConfiguration(), vararg configElements: ConfigurationTemplate ) { private val developerMode = true - private val useHttps = false + val cordaX500Name: CordaX500Name by lazy({ + CordaX500Name(name, location, country) + }) private val basicConfig = """ |myLegalName="C=$country,L=$location,O=$name" |keyStorePassword="cordacadevpass" |trustStorePassword="trustpass" - |extraAdvertisedServiceIds=[ "" ] - |useHTTPS=$useHttps |devMode=$developerMode |jarDirs = [ "../libs" ] """.trimMargin() @@ -41,6 +45,7 @@ class Configuration( fun writeToFile(file: File) { FileUtils.writeStringToFile(file, this.generate(), "UTF-8") + log.info(this.generate()) } private fun generate() = listOf(basicConfig, database.config(), extraConfig) @@ -48,9 +53,8 @@ class Configuration( .joinToString("\n") companion object { - - private val DEFAULT_PASSWORD = "S0meS3cretW0rd" - + private val log = getLogger() + val DEFAULT_PASSWORD = "S0meS3cretW0rd" } } diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt index e6a2c94be2..37f506f4fd 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt @@ -1,8 +1,8 @@ package net.corda.behave.node.configuration -class CordappConfiguration(vararg apps: String, var includeFinance: Boolean = false) : ConfigurationTemplate() { +class CordappConfiguration(var apps: List = emptyList(), val includeFinance: Boolean = false) : ConfigurationTemplate() { - private val applications = apps.toList() + if (includeFinance) { + private val applications = apps + if (includeFinance) { listOf("net.corda:corda-finance:CORDA_VERSION") } else { emptyList() diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt index fd639141e0..df829bff89 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt @@ -8,9 +8,11 @@ class CurrencyConfiguration(private val issuableCurrencies: List) : Conf "" } else { """ - |issuableCurrencies=[ - | ${issuableCurrencies.joinToString(", ")} - |] + |custom : { + | issuableCurrencies : [ + | ${issuableCurrencies.joinToString(", ")} + | ] + |} """ } } 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 fe09792dd2..e3073668b5 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 @@ -10,7 +10,8 @@ data class NetworkInterface( val rpcPort: Int = getPort(12002 + (nodeIndex * 5)), val rpcAdminPort: Int = getPort(12003 + (nodeIndex * 5)), val webPort: Int = getPort(12004 + (nodeIndex * 5)), - val dbPort: Int = getPort(12005 + (nodeIndex * 5)) + val dbPort: Int = getPort(12005 + (nodeIndex * 5)), + val dockerPort: Int = getPort(5000 + (nodeIndex * 5)) ) : ConfigurationTemplate() { init { @@ -28,7 +29,6 @@ data class NetworkInterface( | address = "$host:$rpcPort" | adminAddress = "$host:$rpcAdminPort" |} - |webAddress="$host:$webPort" """ } 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 58dff6ad6d..dc7c74caa6 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 @@ -1,6 +1,6 @@ package net.corda.behave.node.configuration -class NotaryConfiguration(private val notaryType: NotaryType) : ConfigurationTemplate() { +class NotaryConfiguration(private val notaryType: NotaryType = NotaryType.NONE) : ConfigurationTemplate() { override val config: (Configuration) -> String get() = { diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt b/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt index a35ae287db..5a8b071365 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt @@ -27,12 +27,12 @@ open class Command( private var process: Process? = null - private lateinit var outputListener: OutputListener + private var outputListener: OutputListener? = null var exitCode = -1 private set - val output: Observable = Observable.create { emitter -> + val output: Observable = Observable.create({ emitter -> outputListener = object : OutputListener { override fun onNewLine(line: String) { emitter.onNext(line) @@ -42,10 +42,11 @@ open class Command( emitter.onCompleted() } } - } + }).share() private val thread = Thread(Runnable { try { + log.info("Command: $command") val processBuilder = ProcessBuilder(command) .directory(directory) .redirectErrorStream(true) @@ -57,13 +58,17 @@ open class Command( while (true) { try { val line = input.readLine()?.trimEnd() ?: break - outputListener.onNewLine(line) + log.trace(line) + outputListener?.onNewLine(line) } catch (_: IOException) { break + } catch (ex: Exception) { + log.error("Unexpected exception during reading input", ex) + break } } input.close() - outputListener.onEndOfStream() + outputListener?.onEndOfStream() outputCapturedLatch.countDown() }).start() val streamIsClosed = outputCapturedLatch.await(timeout) @@ -88,13 +93,15 @@ open class Command( } } catch (e: Exception) { log.warn("Error occurred when trying to run process", e) + throw e + } + finally { + process = null + terminationLatch.countDown() } - process = null - terminationLatch.countDown() }) fun start() { - output.subscribe() thread.start() } 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 d465170c17..674d01b3ab 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 @@ -4,7 +4,7 @@ import java.io.File import java.time.Duration class JarCommand( - jarFile: File, + val jarFile: File, arguments: Array, directory: File, timeout: Duration, diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt index 262c0fcb27..71f52a6c5a 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt @@ -6,13 +6,13 @@ import com.spotify.docker.client.messages.ContainerConfig import com.spotify.docker.client.messages.HostConfig import com.spotify.docker.client.messages.PortBinding import net.corda.behave.monitoring.PatternWatch -import net.corda.behave.monitoring.Watch import rx.Observable import java.io.Closeable abstract class ContainerService( name: String, port: Int, + val startupStatement: String, settings: ServiceSettings = ServiceSettings() ) : Service(name, port, settings), Closeable { @@ -30,8 +30,6 @@ abstract class ContainerService( private val environmentVariables: MutableList = mutableListOf() - private var startupStatement: Watch = PatternWatch.EMPTY - private val imageReference: String get() = "$baseImage:$imageTag" @@ -51,7 +49,12 @@ abstract class ContainerService( val creation = client.createContainer(containerConfig) id = creation.id() + + val info = client.inspectContainer(id) + log.info("Container $id info: $info") + client.startContainer(id) + true } catch (e: Exception) { id = null @@ -73,10 +76,6 @@ abstract class ContainerService( environmentVariables.add("$name=$value") } - protected fun setStartupStatement(statement: String) { - startupStatement = PatternWatch(statement) - } - override fun checkPrerequisites() { if (!client.listImages().any { true == it.repoTags()?.contains(imageReference) }) { log.info("Pulling image $imageReference ...") @@ -97,8 +96,8 @@ abstract class ContainerService( while (timeout > 0) { client.logs(id, DockerClient.LogsParam.stdout(), DockerClient.LogsParam.stderr()).use { val contents = it.readFully() - val observable = Observable.from(contents.split("\n")) - if (startupStatement.await(observable, settings.pollInterval)) { + val observable = Observable.from(contents.split("\n", "\r")) + if (PatternWatch(observable, startupStatement).await(settings.pollInterval)) { log.info("Found process start-up statement for {}", this) return true } @@ -118,5 +117,4 @@ abstract class ContainerService( client.close() } } - } 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/PostgreSQLService.kt similarity index 60% rename from experimental/behave/src/main/kotlin/net/corda/behave/service/database/SqlServerService.kt rename to experimental/behave/src/main/kotlin/net/corda/behave/service/database/PostgreSQLService.kt index 6a18df586f..b42f783569 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/service/database/SqlServerService.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/database/PostgreSQLService.kt @@ -2,31 +2,25 @@ package net.corda.behave.service.database import net.corda.behave.database.DatabaseConnection import net.corda.behave.database.DatabaseType -import net.corda.behave.database.configuration.SqlServerConfigurationTemplate +import net.corda.behave.database.configuration.PostgresConfigurationTemplate import net.corda.behave.node.configuration.DatabaseConfiguration import net.corda.behave.service.ContainerService import net.corda.behave.service.ServiceSettings -class SqlServerService( +class PostgreSQLService( name: String, port: Int, private val password: String, settings: ServiceSettings = ServiceSettings() -) : ContainerService(name, port, settings) { +) : ContainerService(name, port, "database system is ready to accept connections", settings) { - override val baseImage = "microsoft/mssql-server-linux" + override val baseImage = "postgres" - override val internalPort = 1433 - - init { - addEnvironmentVariable("ACCEPT_EULA", "Y") - addEnvironmentVariable("SA_PASSWORD", password) - setStartupStatement("SQL Server is now ready for client connections") - } + override val internalPort = 5432 override fun verify(): Boolean { val config = DatabaseConfiguration( - type = DatabaseType.SQL_SERVER, + type = DatabaseType.POSTGRES, host = host, port = port, database = database, @@ -34,7 +28,7 @@ class SqlServerService( username = username, password = password ) - val connection = DatabaseConnection(config, SqlServerConfigurationTemplate()) + val connection = DatabaseConnection(config, PostgresConfigurationTemplate()) try { connection.use { return true @@ -47,12 +41,11 @@ class SqlServerService( } companion object { - val host = "localhost" - val database = "master" - val schema = "dbo" - val username = "sa" - + val database = "postgres" + val schema = "public" + val username = "postgres" + val driver = "postgresql-42.1.4.jar" } } \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt deleted file mode 100644 index f3fad092c9..0000000000 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt +++ /dev/null @@ -1,18 +0,0 @@ -package net.corda.behave.scenarios - -import cucumber.api.java.After -import cucumber.api.java.Before - -@Suppress("KDocMissingDocumentation") -class ScenarioHooks(private val state: ScenarioState) { - - @Before - fun beforeScenario() { - } - - @After - fun afterScenario() { - state.stopNetwork() - } - -} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioRunner.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioRunner.kt new file mode 100644 index 0000000000..62f7bf0680 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioRunner.kt @@ -0,0 +1,59 @@ +@file:JvmName("ScenarioRunner") +package net.corda.behave.scenarios + +import joptsimple.OptionParser +import kotlin.system.exitProcess + +fun main(args: Array) { + val parser = OptionParser() + val featurePath = parser.accepts("path").withRequiredArg().required().ofType(String::class.java) + .describedAs("Path location of .feature specifications") + val glue = parser.accepts("glue").withOptionalArg().ofType(String::class.java) + .describedAs("location of additional step definitions, hooks and plugins") + .defaultsTo("net.corda.behave.scenarios") + val plugin = parser.accepts("plugin").withOptionalArg().ofType(String::class.java) + .describedAs("register additional plugins (see https://cucumber.io/docs/reference/jvm)") + .defaultsTo("pretty") + val tags = parser.accepts("tags").withOptionalArg().ofType(String::class.java) + .describedAs("only run scenarios marked as @") + val dryRun = parser.accepts("d") + + val options = try { + parser.parse(*args) + } catch (e: Exception) { + println(e.message) + printHelp(parser) + exitProcess(1) + } + + val cliArgs = listOf("--glue", + options.valueOf(glue), + "--plugin", + options.valueOf(plugin), + options.valueOf(featurePath)) + + (if (options.hasArgument("tags")) + listOf("--tags", options.valueOf(tags)) + else emptyList()) + + if (options.has(dryRun)) listOf("-d") else emptyList() + + println("Cucumber CLI scenario runner args: $cliArgs") + cucumber.api.cli.Main.main(cliArgs.toTypedArray()) +} + +private fun printHelp(parser: OptionParser) { + println(""" + Usage: ScenarioRunner [options] --path + + Examples: + ScenarioRunner -path + ScenarioRunner -path /.feature + ScenarioRunner -path /.feature:3:9 + + ScenarioRunner -path --plugin html --tags @qa + ScenarioRunner -path --plugin html --tags @compatibility + + Please refer to the Cucumber documentation https://cucumber.io/docs/reference/jvm for more info. + + """.trimIndent()) + parser.printHelpOn(System.out) +} \ 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 f6cfb32298..d39e4c543a 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 @@ -1,10 +1,12 @@ package net.corda.behave.scenarios +import cucumber.api.java.After import net.corda.behave.logging.getLogger import net.corda.behave.network.Network import net.corda.behave.node.Node import net.corda.core.messaging.CordaRPCOps import org.assertj.core.api.Assertions.assertThat +import java.time.Duration class ScenarioState { @@ -36,7 +38,7 @@ class ScenarioState { return nodes.firstOrNull { it.name == nodeName(name) } ?: newNode(name) } - fun ensureNetworkIsRunning() { + fun ensureNetworkIsRunning(timeout: Duration? = null) { if (network != null) { // Network is already running return @@ -47,7 +49,7 @@ class ScenarioState { } network = networkBuilder.generate() network?.start() - assertThat(network?.waitUntilRunning()).isTrue() + assertThat(network?.waitUntilRunning(timeout)).isTrue() } inline fun withNetwork(action: ScenarioState.() -> T): T { @@ -63,6 +65,7 @@ class ScenarioState { } } + @After fun stopNetwork() { val network = network ?: return for (node in network) { @@ -74,7 +77,7 @@ class ScenarioState { network.stop() } - private fun nodeName(name: String) = "Entity$name" + private fun nodeName(name: String) = "$name" private fun newNode(name: String): Node.Builder { val builder = Node.new() diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsBlock.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsBlock.kt deleted file mode 100644 index 5880c939a3..0000000000 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsBlock.kt +++ /dev/null @@ -1,3 +0,0 @@ -package net.corda.behave.scenarios - -typealias StepsBlock = (StepsContainer.() -> Unit) -> Unit \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt index 818c08916a..e37e9c475d 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt @@ -1,60 +1,50 @@ package net.corda.behave.scenarios import cucumber.api.java8.En -import net.corda.behave.scenarios.helpers.Cash -import net.corda.behave.scenarios.helpers.Database -import net.corda.behave.scenarios.helpers.Ssh -import net.corda.behave.scenarios.helpers.Startup +import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner +import net.corda.behave.scenarios.api.StepsBlock +import net.corda.behave.scenarios.api.StepsProvider import net.corda.behave.scenarios.steps.* -import net.corda.core.messaging.CordaRPCOps -import org.slf4j.Logger -import org.slf4j.LoggerFactory +import net.corda.core.internal.objectOrNewInstance +import net.corda.core.utilities.loggerFor @Suppress("KDocMissingDocumentation") class StepsContainer(val state: ScenarioState) : En { - private val log: Logger = LoggerFactory.getLogger(StepsContainer::class.java) + companion object { + val stepsProviders: List by lazy { + FastClasspathScanner().addClassLoader(this::class.java.classLoader).scan() + .getNamesOfClassesImplementing(StepsProvider::class.java) + .mapNotNull { this::class.java.classLoader.loadClass(it).asSubclass(StepsProvider::class.java) } + .map { it.kotlin.objectOrNewInstance() } + } + } - private val stepDefinitions: List<(StepsBlock) -> Unit> = listOf( - ::cashSteps, - ::configurationSteps, - ::databaseSteps, - ::networkSteps, - ::rpcSteps, - ::sshSteps, - ::startupSteps + private val log = loggerFor() + + private val stepDefinitions: List = listOf( + CashSteps(), + ConfigurationSteps(), + DatabaseSteps(), + NetworkSteps(), + RpcSteps(), + SshSteps(), + StartupSteps(), + VaultSteps() ) init { - stepDefinitions.forEach { it({ this.steps(it) }) } + log.info("Initialising common Steps Provider ...") + stepDefinitions.forEach { it.initialize(state) } + log.info("Searching and registering custom Steps Providers ...") + stepsProviders.forEach { stepsProvider -> + val stepsDefinition = stepsProvider.stepsDefinition + log.info("Registering: $stepsDefinition") + stepsDefinition.initialize(state) + } } - fun succeed() = log.info("Step succeeded") - - fun fail(message: String) = state.fail(message) - - fun error(message: String) = state.error(message) - - fun node(name: String) = state.nodeBuilder(name) - - fun withNetwork(action: ScenarioState.() -> Unit) { - state.withNetwork(action) - } - - fun withClient(nodeName: String, action: (CordaRPCOps) -> T): T { - return state.withClient(nodeName, action) - } - - val startup = Startup(state) - - val database = Database(state) - - val ssh = Ssh(state) - - val cash = Cash(state) - - private fun steps(action: (StepsContainer.() -> Unit)) { + fun steps(action: (StepsContainer.() -> Unit)) { action(this) } - } diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/api/StepsBlock.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/api/StepsBlock.kt new file mode 100644 index 0000000000..cdb711b373 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/api/StepsBlock.kt @@ -0,0 +1,16 @@ +package net.corda.behave.scenarios.api + +import cucumber.api.java8.En +import net.corda.behave.scenarios.ScenarioState +import net.corda.core.utilities.contextLogger + +interface StepsBlock : En { + + companion object { + val log = contextLogger() + } + + fun initialize(state: ScenarioState) + + fun succeed() = log.info("Step succeeded") +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/api/StepsProvider.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/api/StepsProvider.kt new file mode 100644 index 0000000000..fb72a0e951 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/api/StepsProvider.kt @@ -0,0 +1,6 @@ +package net.corda.behave.scenarios.api + +interface StepsProvider { + val name: String + val stepsDefinition: StepsBlock +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt index 0b59771164..a989abceed 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt @@ -1,8 +1,16 @@ package net.corda.behave.scenarios.helpers import net.corda.behave.scenarios.ScenarioState +import net.corda.core.CordaRuntimeException +import net.corda.core.contracts.Amount import net.corda.core.messaging.startFlow +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow import net.corda.finance.flows.CashConfigDataFlow +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow +import java.util.* import java.util.concurrent.TimeUnit class Cash(state: ScenarioState) : Substeps(state) { @@ -28,4 +36,34 @@ class Cash(state: ScenarioState) : Substeps(state) { } } + fun issueCash(issueToNode: String, amount: Long, currency: String): SignedTransaction { + return withClient(issueToNode) { + try { + val notaryList = it.notaryIdentities() + if (notaryList.isEmpty()) + throw CordaRuntimeException("No Notaries configured in this network.") + val notaryParty = notaryList[0] + return@withClient it.startFlow(::CashIssueFlow, Amount(amount, Currency.getInstance(currency)), OpaqueBytes.of(1), notaryParty).returnValue.getOrThrow().stx + } catch (ex: Exception) { + log.warn("Failed to issue $amount $currency cash to $issueToNode", ex) + throw ex + } + } + } + + fun transferCash(senderNode: String, sendToNode: String, amount: Long, currency: String): SignedTransaction { + return withClient(senderNode) { + try { + val sendToX500Name = node(sendToNode).config.cordaX500Name + val sendToParty = node(senderNode).rpc { + it.wellKnownPartyFromX500Name(sendToX500Name) ?: throw IllegalStateException("Unable to locate $sendToX500Name in Network Map Service") + } + return@withClient it.startFlow(::CashPaymentFlow, Amount(amount, Currency.getInstance(currency)), sendToParty).returnValue.getOrThrow().stx + } catch (ex: Exception) { + log.warn("Failed to transfer $amount cash from $senderNode to $sendToNode", ex) + throw ex + } + } + } + } \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Startup.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Startup.kt index 3520acec43..cc9bfd1633 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Startup.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Startup.kt @@ -1,6 +1,10 @@ package net.corda.behave.scenarios.helpers +import net.corda.behave.file.div +import net.corda.behave.minutes +import net.corda.behave.process.JarCommand import net.corda.behave.scenarios.ScenarioState +import java.io.File class Startup(state: ScenarioState) : Substeps(state) { @@ -10,6 +14,13 @@ class Startup(state: ScenarioState) : Substeps(state) { if (!node(nodeName).nodeInfoGenerationOutput.find("Logs can be found in.*").any()) { fail("Unable to find logging information for node $nodeName") } + + withClient(nodeName) { + log.info("$nodeName: ${it.nodeInfo()} has registered flows:") + for (flow in it.registeredFlows()) { + log.info(flow) + } + } } } @@ -22,6 +33,19 @@ class Startup(state: ScenarioState) : Substeps(state) { } } + fun hasIdentityDetails(nodeName: String) { + withNetwork { + log.info("Retrieving identity details for node '$nodeName' ...") + try { + val nodeInfo = node(nodeName).rpc { it.nodeInfo() } + log.info("\nNode $nodeName identity details: $nodeInfo\n") + } catch (ex: Exception) { + log.warn("Failed to retrieve node identity details", ex) + throw ex + } + } + } + fun hasPlatformVersion(nodeName: String, platformVersion: Int) { withNetwork { log.info("Finding platform version for node '$nodeName' ...") @@ -51,7 +75,7 @@ class Startup(state: ScenarioState) : Substeps(state) { if (match == null) { fail("Unable to find version for node '$nodeName'") } else { - val foundVersion = Regex("Version: ([^ ]+) ") + val foundVersion = Regex("Release: ([^ ]+) ") .find(match.contents) ?.groups?.last()?.value fail("Expected version $version for node '$nodeName', " + @@ -62,4 +86,29 @@ class Startup(state: ScenarioState) : Substeps(state) { } } + fun hasLoadedCordapp(nodeName: String, cordappName: String) { + withNetwork { + log.info("Checking CorDapp $cordappName is loaded in node $nodeName ...\n") + val logOutput = node(nodeName).logOutput + if (!logOutput.find(".*Loaded CorDapps.*$cordappName.*").any()) { + fail("Unable to find $cordappName loaded in node $nodeName") + } + } + } + + fun runCordapp(nodeName: String, cordapp: String, vararg args: String) { + withNetwork { + val cordaApp = node(nodeName).config.cordapps.apps.find { it.contains(cordapp) } ?: fail("Unable to locate CorDapp: $cordapp") + // launch cordapp jar + // assumption is there is a Main() method declared in the manifest of the JAR + // eg. Main-Class: net.corda.notaryhealthcheck.MainKt + val cordappDirectory = node(nodeName).config.distribution.cordappDirectory + val cordappJar : File = cordappDirectory / "$cordapp.jar" + // Execute + val command = JarCommand(cordappJar, args as Array, cordappDirectory, 1.minutes) + command.start() + if (!command.waitFor()) + fail("Failed to successfully run the CorDapp jar: $cordaApp") + } + } } \ No newline at end of file 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 bba2f052e0..c2fbb45a1c 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 @@ -20,5 +20,4 @@ abstract class Substeps(protected val state: ScenarioState) { } }) } - } \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Vault.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Vault.kt new file mode 100644 index 0000000000..8f9b96712a --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Vault.kt @@ -0,0 +1,21 @@ +package net.corda.behave.scenarios.helpers + +import net.corda.behave.scenarios.ScenarioState +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef + +class Vault(state: ScenarioState) : Substeps(state) { + + fun query(nodeName: String, contractStateType: Class): List>{ + return withClient(nodeName) { + try { + val results = it.vaultQuery(contractStateType) + log.info("Vault query return results: $results") + return@withClient results.states + } catch (ex: Exception) { + log.warn("Failed to retrieve cash configuration data", ex) + throw ex + } + } + } +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/CashSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/CashSteps.kt index e26486c351..b2c3059511 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/CashSteps.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/CashSteps.kt @@ -1,20 +1,37 @@ package net.corda.behave.scenarios.steps -import net.corda.behave.scenarios.StepsBlock +import net.corda.behave.scenarios.ScenarioState +import net.corda.behave.scenarios.api.StepsBlock +import net.corda.behave.scenarios.helpers.Cash import org.assertj.core.api.Assertions.assertThat -fun cashSteps(steps: StepsBlock) = steps { +class CashSteps : StepsBlock { - Then("^node (\\w+) has 1 issuable currency$") { name -> - withNetwork { - assertThat(cash.numberOfIssuableCurrencies(name)).isEqualTo(1) + override fun initialize(state: ScenarioState) { + val cash = Cash(state) + + Then("^node (\\w+) has 1 issuable currency$") { name -> + state.withNetwork { + assertThat(cash.numberOfIssuableCurrencies(name)).isEqualTo(1) + } + } + + Then("^node (\\w+) has (\\w+) issuable currencies$") { name, count -> + state.withNetwork { + assertThat(cash.numberOfIssuableCurrencies(name)).isEqualTo(count.toInt()) + } + } + + Then("^node (\\w+) can transfer (\\d+) (\\w+) to node (\\w+)$") { nodeA, amount, currency, nodeB -> + state.withNetwork { + cash.transferCash(nodeA, nodeB, amount, currency) + } + } + + Then("^node (\\w+) can issue (\\d+) (\\w+)$") { nodeA, amount, currency -> + state.withNetwork { + cash.issueCash(nodeA, amount, currency) + } } } - - Then("^node (\\w+) has (\\w+) issuable currencies$") { name, count -> - withNetwork { - assertThat(cash.numberOfIssuableCurrencies(name)).isEqualTo(count.toInt()) - } - } - } 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 27def111b8..5a87b21162 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 @@ -3,47 +3,64 @@ package net.corda.behave.scenarios.steps import net.corda.behave.database.DatabaseType import net.corda.behave.node.Distribution import net.corda.behave.node.configuration.toNotaryType -import net.corda.behave.scenarios.StepsBlock +import net.corda.behave.scenarios.ScenarioState +import net.corda.behave.scenarios.api.StepsBlock -fun configurationSteps(steps: StepsBlock) = steps { +class ConfigurationSteps : StepsBlock { + + override fun initialize(state: ScenarioState) { + fun node(name: String) = state.nodeBuilder(name) + + Given("^a node (\\w+) of version ([^ ]+)$") { name, version -> + node(name) + .withDistribution(Distribution.fromVersionString(version)) + } + + Given("^a node (\\w+) in location (\\w+) and country (\\w+) of version ([^ ]+)$") { name, location, country, version -> + node(name) + .withDistribution(Distribution.fromVersionString(version)) + .withLocation(location.replace("_", " "), country) + } + + Given("^a (\\w+) notary node (\\w+) of version ([^ ]+)$") { notaryType, name, version -> + node(name) + .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)) + .withNotaryType(type.toNotaryType() + ?: error("Unknown notary type '$type'")) + } + + Given("^node (\\w+) uses database of type (.+)$") { name, type -> + node(name) + .withDatabaseType(DatabaseType.fromName(type) + ?: error("Unknown database type '$type'")) + } + + Given("^node (\\w+) can issue currencies of denomination (.+)$") { name, currencies -> + node(name).withIssuableCurrencies(currencies + .replace(" and ", ", ") + .split(", ") + .map { it.toUpperCase() }) + } + + Given("^node (\\w+) is located in (\\w+), (\\w+)$") { name, location, country -> + node(name).withLocation(location, country) + } + + Given("^node (\\w+) has the finance app installed$") { name -> + node(name).withFinanceApp() + } + + Given("^node (\\w+) has app installed: (.+)$") { name, app -> + node(name).withApp(app) + } - Given("^a node (\\w+) of version ([^ ]+)$") { name, version -> - node(name) - .withDistribution(Distribution.fromVersionString(version) - ?: error("Unknown version '$version'")) } - - Given("^a (\\w+) notary (\\w+) of version ([^ ]+)$") { type, name, version -> - node(name) - .withDistribution(Distribution.fromVersionString(version) - ?: error("Unknown version '$version'")) - .withNotaryType(type.toNotaryType() - ?: error("Unknown notary type '$type'")) - } - - Given("^node (\\w+) uses database of type (.+)$") { name, type -> - node(name) - .withDatabaseType(DatabaseType.fromName(type) - ?: error("Unknown database type '$type'")) - } - - Given("^node (\\w+) can issue (.+)$") { name, currencies -> - node(name).withIssuableCurrencies(currencies - .replace(" and ", ", ") - .split(", ") - .map { it.toUpperCase() }) - } - - Given("^node (\\w+) is located in (\\w+), (\\w+)$") { name, location, country -> - node(name).withLocation(location, country) - } - - Given("^node (\\w+) has the finance app installed$") { name -> - node(name).withFinanceApp() - } - - Given("^node (\\w+) has app installed: (.+)$") { name, app -> - node(name).withApp(app) - } - } + diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DatabaseSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DatabaseSteps.kt index 9b21650a50..a3deaf0670 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DatabaseSteps.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DatabaseSteps.kt @@ -1,13 +1,18 @@ package net.corda.behave.scenarios.steps -import net.corda.behave.scenarios.StepsBlock +import net.corda.behave.scenarios.ScenarioState +import net.corda.behave.scenarios.api.StepsBlock +import net.corda.behave.scenarios.helpers.Database -fun databaseSteps(steps: StepsBlock) = steps { +class DatabaseSteps : StepsBlock { - Then("^user can connect to the database of node (\\w+)$") { name -> - withNetwork { - database.canConnectTo(name) + override fun initialize(state: ScenarioState) { + val database = Database(state) + + Then("^user can connect to the database of node (\\w+)$") { name -> + state.withNetwork { + database.canConnectTo(name) + } } } - } diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/NetworkSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/NetworkSteps.kt index fcc544de29..bf9a01642c 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/NetworkSteps.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/NetworkSteps.kt @@ -1,11 +1,18 @@ package net.corda.behave.scenarios.steps -import net.corda.behave.scenarios.StepsBlock +import net.corda.behave.scenarios.ScenarioState +import net.corda.behave.scenarios.api.StepsBlock +import net.corda.core.utilities.minutes -fun networkSteps(steps: StepsBlock) = steps { +class NetworkSteps : StepsBlock { - When("^the network is ready$") { - state.ensureNetworkIsRunning() + override fun initialize(state: ScenarioState) { + When("^the network is ready$") { + state.ensureNetworkIsRunning() + } + + When("^the network is ready within (\\d+) minutes$") { minutes -> + state.ensureNetworkIsRunning(minutes.minutes) + } } - } diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/RpcSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/RpcSteps.kt index 9accb28398..686e62b6d8 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/RpcSteps.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/RpcSteps.kt @@ -1,13 +1,15 @@ package net.corda.behave.scenarios.steps -import net.corda.behave.scenarios.StepsBlock +import net.corda.behave.scenarios.ScenarioState +import net.corda.behave.scenarios.api.StepsBlock -fun rpcSteps(steps: StepsBlock) = steps { +class RpcSteps : StepsBlock { - Then("^user can connect to node (\\w+) using RPC$") { name -> - withClient(name) { - succeed() + override fun initialize(state: ScenarioState) { + Then("^user can connect to node (\\w+) using RPC$") { name -> + state.withClient(name) { + succeed() + } } } - } diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/SshSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/SshSteps.kt index 516732f1e7..0e4656b1a8 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/SshSteps.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/SshSteps.kt @@ -1,13 +1,18 @@ package net.corda.behave.scenarios.steps -import net.corda.behave.scenarios.StepsBlock +import net.corda.behave.scenarios.ScenarioState +import net.corda.behave.scenarios.api.StepsBlock +import net.corda.behave.scenarios.helpers.Ssh -fun sshSteps(steps: StepsBlock) = steps { +class SshSteps : StepsBlock { - Then("^user can connect to node (\\w+) using SSH$") { name -> - withNetwork { - ssh.canConnectTo(name) + override fun initialize(state: ScenarioState) { + val ssh = Ssh(state) + + Then("^user can connect to node (\\w+) using SSH$") { name -> + state.withNetwork { + ssh.canConnectTo(name) + } } } - } diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/StartupSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/StartupSteps.kt index f78415f2c8..73e7e37e5a 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/StartupSteps.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/StartupSteps.kt @@ -1,31 +1,66 @@ package net.corda.behave.scenarios.steps -import net.corda.behave.scenarios.StepsBlock +import net.corda.behave.scenarios.ScenarioState +import net.corda.behave.scenarios.api.StepsBlock +import net.corda.behave.scenarios.helpers.Startup -fun startupSteps(steps: StepsBlock) = steps { +class StartupSteps : StepsBlock { - Then("^user can retrieve database details for node (\\w+)$") { name -> - withNetwork { - startup.hasDatabaseDetails(name) + override fun initialize(state: ScenarioState) { + val startup = Startup(state) + + Then("^user can retrieve database details for node (\\w+)$") { name -> + state.withNetwork { + startup.hasDatabaseDetails(name) + } + } + + Then("^user can retrieve logging information for node (\\w+)$") { name -> + state.withNetwork { + startup.hasLoggingInformation(name) + } + } + + Then("^node (\\w+) is on release version ([^ ]+)$") { name, version -> + state.withNetwork { + startup.hasVersion(name, version) + } + } + + Then("^node (\\w+) is on platform version (\\w+)$") { name, platformVersion -> + state.withNetwork { + startup.hasPlatformVersion(name, platformVersion.toInt()) + } + } + + Then("^user can retrieve node identity information for node (\\w+)") { name -> + state.withNetwork { + startup.hasIdentityDetails(name) + } + } + + Then("^node (\\w+) has loaded app (.+)$") { name, cordapp -> + state.withNetwork { + startup.hasLoadedCordapp(name, cordapp) + } + } + + Then("^node (\\w+) can run (\\w+)\$") { name, cordapp -> + state.withNetwork { + startup.runCordapp(name, cordapp) + } + } + + Then("^node (\\w+) can run (\\w+) (\\w+)\$") { name, cordapp, arg1 -> + state.withNetwork { + startup.runCordapp(name, cordapp, arg1) + } + } + + Then("^node (\\w+) can run (\\w+) (\\w+) (\\w+)\$") { name, cordapp, arg1, arg2 -> + state.withNetwork { + startup.runCordapp(name, cordapp, arg1, arg2) + } } } - - Then("^user can retrieve logging information for node (\\w+)$") { name -> - withNetwork { - startup.hasLoggingInformation(name) - } - } - - Then("^node (\\w+) is on version ([^ ]+)$") { name, version -> - withNetwork { - startup.hasVersion(name, version) - } - } - - Then("^node (\\w+) is on platform version (\\w+)$") { name, platformVersion -> - withNetwork { - startup.hasPlatformVersion(name, platformVersion.toInt()) - } - } - } \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/VaultSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/VaultSteps.kt new file mode 100644 index 0000000000..4a2b7b3c3c --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/VaultSteps.kt @@ -0,0 +1,44 @@ +package net.corda.behave.scenarios.steps + +import net.corda.behave.scenarios.ScenarioState +import net.corda.behave.scenarios.api.StepsBlock +import net.corda.behave.scenarios.helpers.Vault +import net.corda.core.contracts.ContractState +import net.corda.core.utilities.sumByLong +import net.corda.finance.contracts.asset.Cash + +class VaultSteps : StepsBlock { + + override fun initialize(state: ScenarioState) { + val vault = Vault(state) + + Then("^node (\\w+) vault contains (\\d+) states$") { node, count -> + if (vault.query(node, ContractState::class.java).size == count) + succeed() + else + state.fail("Vault on node $node does not contain expected number of states: $count") + } + + Then("^node (\\w+) vault contains (\\d+) (\\w+) states$") { node, count, contractType -> + try { + val contractStateTypeClass = Class.forName(contractType) as Class + if (vault.query(node, contractStateTypeClass).size == count) + succeed() + else + state.fail("Vault on node $node does not contain expected number of states: $count") + } catch (e: Exception) { + state.fail("Invalid contract state class type: ${e.message}") + } + } + + Then("^node (\\w+) vault contains total cash of (\\d+) (\\w+)$") { node, total, currency -> + val cashStates = vault.query(node, Cash.State::class.java) + val sumCashStates = cashStates.filter { it.state.data.amount.token.product.currencyCode == currency }?.sumByLong { it.state.data.amount.quantity } + print((sumCashStates)) + if (sumCashStates == total) + succeed() + else + state.fail("Vault on node $node does not contain total cash of : $total") + } + } +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/tests/StepsProviderTests.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/tests/StepsProviderTests.kt new file mode 100644 index 0000000000..768eafd43a --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/tests/StepsProviderTests.kt @@ -0,0 +1,32 @@ +package net.corda.behave.scenarios.tests + +import net.corda.behave.scenarios.ScenarioState +import net.corda.behave.scenarios.StepsContainer +import net.corda.behave.scenarios.api.StepsBlock +import net.corda.behave.scenarios.api.StepsProvider +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class StepsProviderTests { + + @Test + fun `module can discover steps providers`() { + + val foundProviders = StepsContainer.Companion.stepsProviders + assertThat(foundProviders).hasOnlyElementsOfType(FooStepsProvider::class.java).hasSize(1) + } + + class FooStepsProvider : StepsProvider { + + override val name: String + get() = "Foo" + + override val stepsDefinition: StepsBlock + get() = DummyStepsBlock() + } + + class DummyStepsBlock : StepsBlock { + override fun initialize(state: ScenarioState) { + } + } +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/resources/features/cash/currencies.feature b/experimental/behave/src/scenario/resources/features/cash/currencies.feature index 3085ced84e..e5fda9eecb 100644 --- a/experimental/behave/src/scenario/resources/features/cash/currencies.feature +++ b/experimental/behave/src/scenario/resources/features/cash/currencies.feature @@ -3,13 +3,21 @@ Feature: Cash - Issuable Currencies To have cash on ledger, certain nodes must have the ability to issue cash of various currencies. Scenario: Node can issue no currencies by default - Given a node A of version master - And node A has the finance app installed + Given a node PartyA of version master + And node PartyA has the finance app installed When the network is ready - Then node A has 0 issuable currencies + Then node PartyA has 0 issuable currencies + + Scenario: Node has an issuable currency + Given a node PartyA of version master + And node PartyA can issue currencies of denomination USD + And node PartyA has the finance app installed + When the network is ready + Then node PartyA has 1 issuable currency Scenario: Node can issue a currency - Given a node A of version master - And node A can issue USD + Given a node PartyA of version master + And a nonvalidating notary Notary of version master + And node PartyA has the finance app installed When the network is ready - Then node A has 1 issuable currency \ No newline at end of file + Then node PartyA can issue 100 USD \ No newline at end of file diff --git a/experimental/behave/src/scenario/resources/features/database/connection.feature b/experimental/behave/src/scenario/resources/features/database/connection.feature index 9be4fc7d2d..e9a48fa1d3 100644 --- a/experimental/behave/src/scenario/resources/features/database/connection.feature +++ b/experimental/behave/src/scenario/resources/features/database/connection.feature @@ -3,12 +3,12 @@ Feature: Database - Connection For Corda to work, a database must be running and appropriately configured. Scenario Outline: User can connect to node's database - Given a node A of version - And node A uses database of type + Given a node PartyA of version + And node PartyA uses database of type When the network is ready - Then user can connect to the database of node A + Then user can connect to the database of node PartyA Examples: | Node-Version | Database-Type | - | MASTER | H2 | - #| MASTER | SQL Server | \ No newline at end of file + | master | H2 | +# | master | postgreSQL | \ 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 20531237c6..02dbc31566 100644 --- a/experimental/behave/src/scenario/resources/features/startup/logging.feature +++ b/experimental/behave/src/scenario/resources/features/startup/logging.feature @@ -4,18 +4,18 @@ Feature: Startup Information - Logging configure / connect relevant software to said node. Scenario: Node shows logging information on startup - Given a node A of version MASTER - And node A uses database of type H2 - And node A is located in London, GB + Given a node PartyA of version master + And node PartyA uses database of type H2 + And node PartyA is located in London, GB When the network is ready - Then user can retrieve logging information for node A + Then user can retrieve logging information for node PartyA Scenario: Node shows database details on startup - Given a node A of version MASTER + Given a node PartyA of version master When the network is ready - Then user can retrieve database details for node A + Then user can retrieve database details for node PartyA Scenario: Node shows version information on startup - Given a node A of version MASTER - Then node A is on platform version 2 - And node A is on version 3.0-SNAPSHOT + Given a node PartyA of version master + Then node PartyA is on platform version 4 + And node PartyA is on release version corda-4.0-snapshot diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/monitoring/MonitoringTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/monitoring/MonitoringTests.kt index 23e0718b6d..5bf5861a13 100644 --- a/experimental/behave/src/test/kotlin/net/corda/behave/monitoring/MonitoringTests.kt +++ b/experimental/behave/src/test/kotlin/net/corda/behave/monitoring/MonitoringTests.kt @@ -10,55 +10,55 @@ class MonitoringTests { @Test fun `watch gets triggered when pattern is observed`() { val observable = Observable.just("first", "second", "third") - val result = PatternWatch("c.n").await(observable, 1.second) + val result = PatternWatch(observable, "c.n").await(1.second) assertThat(result).isTrue() } @Test fun `watch does not get triggered when pattern is not observed`() { val observable = Observable.just("first", "second", "third") - val result = PatternWatch("forth").await(observable, 1.second) + val result = PatternWatch(observable, "forth").await(1.second) assertThat(result).isFalse() } @Test fun `conjunctive watch gets triggered when all its constituents match on the input`() { val observable = Observable.just("first", "second", "third") - val watch1 = PatternWatch("fir") - val watch2 = PatternWatch("ond") - val watch3 = PatternWatch("ird") + val watch1 = PatternWatch(observable, "fir") + val watch2 = PatternWatch(observable, "ond") + val watch3 = PatternWatch(observable, "ird") val aggregate = watch1 * watch2 * watch3 - assertThat(aggregate.await(observable, 1.second)).isTrue() + assertThat(aggregate.await(1.second)).isTrue() } @Test fun `conjunctive watch does not get triggered when one or more of its constituents do not match on the input`() { val observable = Observable.just("first", "second", "third") - val watch1 = PatternWatch("fir") - val watch2 = PatternWatch("ond") - val watch3 = PatternWatch("baz") + val watch1 = PatternWatch(observable, "fir") + val watch2 = PatternWatch(observable, "ond") + val watch3 = PatternWatch(observable, "baz") val aggregate = watch1 * watch2 * watch3 - assertThat(aggregate.await(observable, 1.second)).isFalse() + assertThat(aggregate.await(1.second)).isFalse() } @Test fun `disjunctive watch gets triggered when one or more of its constituents match on the input`() { val observable = Observable.just("first", "second", "third") - val watch1 = PatternWatch("foo") - val watch2 = PatternWatch("ond") - val watch3 = PatternWatch("bar") + val watch1 = PatternWatch(observable, "foo") + val watch2 = PatternWatch(observable, "ond") + val watch3 = PatternWatch(observable, "bar") val aggregate = watch1 / watch2 / watch3 - assertThat(aggregate.await(observable, 1.second)).isTrue() + assertThat(aggregate.await(1.second)).isTrue() } @Test fun `disjunctive watch does not get triggered when none its constituents match on the input`() { val observable = Observable.just("first", "second", "third") - val watch1 = PatternWatch("foo") - val watch2 = PatternWatch("baz") - val watch3 = PatternWatch("bar") + val watch1 = PatternWatch(observable, "foo") + val watch2 = PatternWatch(observable, "baz") + val watch3 = PatternWatch(observable, "bar") val aggregate = watch1 / watch2 / watch3 - assertThat(aggregate.await(observable, 1.second)).isFalse() + assertThat(aggregate.await(1.second)).isFalse() } } \ 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 93801b1d8d..e901b84560 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 @@ -29,7 +29,7 @@ class NetworkTests { val network = Network .new() .addNode("Foo") - .addNode("Bar", databaseType = DatabaseType.SQL_SERVER) + .addNode("Bar", databaseType = DatabaseType.POSTGRES) .addNode("Baz", notaryType = NotaryType.NON_VALIDATING) .generate() network.use { 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 4395ddb83a..7495a98c4d 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 @@ -1,6 +1,6 @@ package net.corda.behave.process -import org.assertj.core.api.Assertions.* +import org.assertj.core.api.Assertions.assertThat import org.junit.Test import rx.observers.TestSubscriber @@ -30,5 +30,4 @@ class CommandTests { } assertThat(exitCode).isEqualTo(0) } - } \ No newline at end of file diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/service/PostreSQLServiceTests.kt similarity index 54% rename from experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt rename to experimental/behave/src/test/kotlin/net/corda/behave/service/PostreSQLServiceTests.kt index 872574cb70..1b823524ec 100644 --- a/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt +++ b/experimental/behave/src/test/kotlin/net/corda/behave/service/PostreSQLServiceTests.kt @@ -1,16 +1,16 @@ package net.corda.behave.service -import net.corda.behave.service.database.SqlServerService +import net.corda.behave.service.database.PostgreSQLService import org.assertj.core.api.Assertions.assertThat import org.junit.Ignore import org.junit.Test -class SqlServerServiceTests { +class PostreSQLServiceTests { @Ignore @Test - fun `sql server can be started and stopped`() { - val service = SqlServerService("test-mssql", 12345, "S0meS3cretW0rd") + fun `postgres can be started and stopped`() { + val service = PostgreSQLService("test-postgres", 12345, "postgres") val didStart = service.start() service.stop() assertThat(didStart).isTrue()