diff --git a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt index ea54a99a91..1cf22f7c66 100644 --- a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt +++ b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt @@ -12,6 +12,7 @@ package net.corda.core.identity import net.corda.core.internal.CertRole import net.corda.core.internal.uncheckedCast +import net.corda.core.internal.validate import net.corda.core.serialization.CordaSerializable import java.security.PublicKey import java.security.cert.* @@ -50,9 +51,7 @@ class PartyAndCertificate(val certPath: CertPath) { /** Verify the certificate path is valid. */ fun verify(trustAnchor: TrustAnchor): PKIXCertPathValidatorResult { - val parameters = PKIXParameters(setOf(trustAnchor)).apply { isRevocationEnabled = false } - val validator = CertPathValidator.getInstance("PKIX") - val result = validator.validate(certPath, parameters) as PKIXCertPathValidatorResult + val result = certPath.validate(trustAnchor) // Apply Corda-specific validity rules to the chain. This only applies to chains with any roles present, so // an all-null chain is in theory valid. var parentRole: CertRole? = CertRole.extract(result.trustAnchor.trustedCert) diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index f83d2b3aec..a7cffcb48b 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -54,7 +54,7 @@ import java.nio.file.attribute.FileTime import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey -import java.security.cert.X509Certificate +import java.security.cert.* import java.time.Duration import java.time.temporal.Temporal import java.util.* @@ -396,6 +396,22 @@ fun ExecutorService.join() { } } +fun CertPath.validate(trustAnchor: TrustAnchor): PKIXCertPathValidatorResult { + val parameters = PKIXParameters(setOf(trustAnchor)).apply { isRevocationEnabled = false } + try { + return CertPathValidator.getInstance("PKIX").validate(this, parameters) as PKIXCertPathValidatorResult + } catch (e: CertPathValidatorException) { + throw CertPathValidatorException( + """Cert path failed to validate against trust anchor. +Reason: ${e.reason} +Offending cert index: ${e.index} +Cert path: $this + +Trust anchor: +$trustAnchor""", e, this, e.index) + } +} + /** * Return the underlying X.500 name from this Corda-safe X.500 name. These are guaranteed to have a consistent * ordering, such that their `toString()` function returns the same value every time for the same [CordaX500Name]. 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 64bd5f6f58..563b97739d 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -139,3 +139,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 5375f98b73..74d827be55 100644 --- a/experimental/behave/build.gradle +++ b/experimental/behave/build.gradle @@ -76,6 +76,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" @@ -95,7 +101,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 1370e15779..71ebc0f91a 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 @@ -26,6 +26,9 @@ class DatabaseSettings { var userName: String = "sa" private set + var driverJar: String? = null + private set + private var databaseConfigTemplate: DatabaseConfigurationTemplate = DatabaseConfigurationTemplate() private val serviceInitiators = mutableListOf() @@ -40,6 +43,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 d301f9ceab..392fba1822 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 @@ -11,11 +11,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) { @@ -29,16 +29,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) @@ -47,9 +50,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 76% 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 6b544c632e..0980813563 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 @@ -13,26 +13,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 235bc5b3e0..fb8e753eb2 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 @@ -15,4 +15,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 3e81d7a5ae..e540885ffe 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 @@ -11,23 +11,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 0d6123bf1b..7257076838 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 @@ -18,16 +18,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 8b282ba6bb..981121b5ee 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 @@ -10,23 +10,25 @@ 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 e210cc8949..48d7640633 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 @@ -16,28 +16,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 6da95832e6..e08dae7df7 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 @@ -14,12 +14,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 @@ -36,8 +38,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 @@ -46,6 +46,10 @@ class Network private constructor( private var hasError = false + init { + FileUtils.forceMkdir(targetDirectory) + } + class Builder internal constructor( private val timeout: Duration ) { @@ -53,7 +57,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()) @@ -61,7 +65,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() @@ -86,22 +90,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) { @@ -119,19 +129,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 } @@ -215,11 +220,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 } @@ -274,6 +283,7 @@ class Network private constructor( } log.info("Shutting down network ...") isStopped = true + log.info("Shutting down nodes ...") for (node in nodes.values) { node.shutDown() } @@ -299,13 +309,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 7deaac0c4d..a4913b0018 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,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 @@ -33,39 +36,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") } } } @@ -73,29 +90,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 @@ -112,15 +125,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 535abbb772..c3db76d368 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 @@ -15,6 +15,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.* @@ -30,6 +31,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 @@ -49,14 +51,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 @@ -86,7 +88,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() } @@ -107,7 +109,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 { @@ -136,7 +138,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 { @@ -226,7 +229,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) @@ -238,7 +241,7 @@ class Node( var name: String? = null private set - private var distribution = Distribution.V3 + private var distribution = Distribution.MASTER private var databaseType = DatabaseType.H2 @@ -324,13 +327,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, @@ -341,7 +345,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 042b10e57e..f54fd31a45 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 @@ -11,13 +11,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", @@ -29,19 +31,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() @@ -51,6 +55,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) @@ -58,9 +63,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 fd4fffd3fa..82292c8d91 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 @@ -10,9 +10,9 @@ 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 d1118c1559..9b7ebc7cd6 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 @@ -18,9 +18,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 2799fd9663..e358da148f 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 @@ -20,7 +20,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 { @@ -38,7 +39,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 9b93277893..63a82ece68 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) : 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 4fa7e39f1b..875a1d3559 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 @@ -37,12 +37,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) @@ -52,10 +52,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) @@ -67,13 +68,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) @@ -98,13 +103,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 eaa7b8acef..99f7141054 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.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 ef2ecbe9bd..5230452e87 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 @@ -16,13 +16,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 { @@ -40,8 +40,6 @@ abstract class ContainerService( private val environmentVariables: MutableList = mutableListOf() - private var startupStatement: Watch = PatternWatch.EMPTY - private val imageReference: String get() = "$baseImage:$imageTag" @@ -61,7 +59,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 @@ -83,10 +86,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 ...") @@ -107,8 +106,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 } @@ -128,5 +127,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 67% 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 cd12b41b64..728d3a5155 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 @@ -12,31 +12,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, @@ -44,7 +38,7 @@ class SqlServerService( username = username, password = password ) - val connection = DatabaseConnection(config, SqlServerConfigurationTemplate()) + val connection = DatabaseConnection(config, PostgresConfigurationTemplate()) try { connection.use { return true @@ -57,12 +51,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 index d00f06978d..f3fad092c9 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt @@ -1,13 +1,3 @@ -/* - * 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.scenarios import cucumber.api.java.After 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 4385359c00..3f6e0d91e0 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 @@ -10,11 +10,13 @@ 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 { @@ -46,7 +48,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 @@ -57,7 +59,7 @@ class ScenarioState { } network = networkBuilder.generate() network?.start() - assertThat(network?.waitUntilRunning()).isTrue() + assertThat(network?.waitUntilRunning(timeout)).isTrue() } inline fun withNetwork(action: ScenarioState.() -> T): T { @@ -73,6 +75,7 @@ class ScenarioState { } } + @After fun stopNetwork() { val network = network ?: return for (node in network) { @@ -84,7 +87,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 index dd63f9e634..e69de29bb2 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsBlock.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsBlock.kt @@ -1,13 +0,0 @@ -/* - * 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.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 67e2cd0161..3f0a5a3830 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 @@ -11,60 +11,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 5ffddd4a3b..9688901833 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 @@ -11,8 +11,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) { @@ -38,4 +46,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 9300afd1ca..5ba3368fa5 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 @@ -10,7 +10,11 @@ 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) { @@ -20,6 +24,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) + } + } } } @@ -32,6 +43,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' ...") @@ -61,7 +85,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', " + @@ -72,4 +96,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 3eb9f14881..d22201bc84 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,5 +30,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 737307cd0a..3b90fb2e85 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 @@ -10,21 +10,38 @@ 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 72499ad081..2b78ed3f1d 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 @@ -13,47 +13,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 0152226d50..2c365c350f 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 @@ -10,14 +10,19 @@ 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 a30d3b4bbc..ec83c488fa 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 @@ -10,12 +10,19 @@ 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 666d3e3421..70a78c79ed 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 @@ -10,14 +10,16 @@ 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 510a390783..8c32648773 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 @@ -10,14 +10,19 @@ 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 02f1bc468b..a9ec6b9d89 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 @@ -10,32 +10,67 @@ 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 cef6bfc038..8cd4308f25 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 @@ -20,55 +20,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 a24768a630..2064aa301c 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 @@ -39,7 +39,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 ecc12db87e..dd4f015946 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 @@ -10,7 +10,7 @@ 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 @@ -40,5 +40,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 74% 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 f329926348..eb7b659a33 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 @@ -10,17 +10,17 @@ 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() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index c4c4f3d57c..73f550d8e8 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -114,20 +114,7 @@ object X509Utilities { } fun validateCertPath(trustedRoot: X509Certificate, certPath: CertPath) { - val params = PKIXParameters(setOf(TrustAnchor(trustedRoot, null))) - params.isRevocationEnabled = false - try { - CertPathValidator.getInstance("PKIX").validate(certPath, params) - } catch (e: CertPathValidatorException) { - throw CertPathValidatorException( - """Cert path failed to validate against root certificate. -Reason: ${e.reason} -Offending cert index: ${e.index} -Cert path: $certPath - -Root certificate: -$trustedRoot""", e, certPath, e.index) - } + certPath.validate(TrustAnchor(trustedRoot, null)) } /**