From a684507553ac51e604a12973691083070cbf180e Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 18 Apr 2018 17:27:03 +0100 Subject: [PATCH 1/7] Better cert path validation exception message for PartyAndCertificate.verify (#2976) --- .../corda/core/identity/PartyAndCertificate.kt | 5 ++--- .../net/corda/core/internal/InternalUtils.kt | 18 +++++++++++++++++- .../nodeapi/internal/crypto/X509Utilities.kt | 15 +-------------- 3 files changed, 20 insertions(+), 18 deletions(-) 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 7f01393eaf..60d11c613f 100644 --- a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt +++ b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt @@ -2,6 +2,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.* @@ -40,9 +41,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 3632d5a9c0..2f3899293f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -44,7 +44,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.* @@ -386,6 +386,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/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 245263768f..85e00d7807 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 @@ -104,20 +104,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)) } /** From ec70478d70d25fab6dd7e550c56b8854d8f2ab54 Mon Sep 17 00:00:00 2001 From: josecoll Date: Thu, 19 Apr 2018 09:56:16 +0100 Subject: [PATCH 2/7] Corda Behave extensions for CTS (#2968) * Updated to Corda 3.0, added support for PostgreSQL, added STAGING_ROOT environment variable, incorporating Watch improvements (Maks), Steps Provider interface for auto-scanning of 3rd party steps providers, re-implemented StepBlocks, new ScenarioRunner executable tool, additional Steps definitions (Vault, issue/transfer cash, cordapps), other minor bug fixes and logging improvements. * Updates incorporating PR review feedback. * Reverted back to original - will re-test in ENT. * Removed all SQL Server code (to be included in ENT only) * Minor updates following second PR review. * Fixed broken scenario tests. * Final fix for PostgreSQL scenario test. --- .../net/corda/core/utilities/KotlinUtils.kt | 5 + experimental/behave/README.md | 2 +- experimental/behave/build.gradle | 7 +- experimental/behave/deps/corda/3.0.0/.gitkeep | 0 .../behave/deps/corda/3.0.0/apps/.gitkeep | 0 experimental/behave/deps/drivers/README.md | 3 +- experimental/behave/prepare.sh | 35 ++++--- .../corda/behave/database/DatabaseSettings.kt | 8 ++ .../net/corda/behave/database/DatabaseType.kt | 24 ++--- ...te.kt => PostgresConfigurationTemplate.kt} | 9 +- .../net/corda/behave/file/FileUtilities.kt | 6 ++ .../behave/monitoring/ConjunctiveWatch.kt | 15 +-- .../behave/monitoring/DisjunctiveWatch.kt | 14 +-- .../corda/behave/monitoring/PatternWatch.kt | 18 ++-- .../net/corda/behave/monitoring/Watch.kt | 50 +++++++--- .../net/corda/behave/network/Network.kt | 49 ++++++---- .../net/corda/behave/node/Distribution.kt | 79 +++++++++------ .../main/kotlin/net/corda/behave/node/Node.kt | 29 +++--- .../node/configuration/Configuration.kt | 18 ++-- .../configuration/CordappConfiguration.kt | 4 +- .../configuration/CurrencyConfiguration.kt | 8 +- .../node/configuration/NetworkInterface.kt | 4 +- .../node/configuration/NotaryConfiguration.kt | 2 +- .../net/corda/behave/process/Command.kt | 23 +++-- .../net/corda/behave/process/JarCommand.kt | 2 +- .../corda/behave/service/ContainerService.kt | 18 ++-- ...lServerService.kt => PostgreSQLService.kt} | 29 +++--- .../corda/behave/scenarios/ScenarioHooks.kt | 18 ---- .../corda/behave/scenarios/ScenarioRunner.kt | 59 +++++++++++ .../corda/behave/scenarios/ScenarioState.kt | 9 +- .../net/corda/behave/scenarios/StepsBlock.kt | 3 - .../corda/behave/scenarios/StepsContainer.kt | 76 +++++++-------- .../corda/behave/scenarios/api/StepsBlock.kt | 16 +++ .../behave/scenarios/api/StepsProvider.kt | 6 ++ .../corda/behave/scenarios/helpers/Cash.kt | 38 ++++++++ .../corda/behave/scenarios/helpers/Startup.kt | 51 +++++++++- .../behave/scenarios/helpers/Substeps.kt | 1 - .../corda/behave/scenarios/helpers/Vault.kt | 21 ++++ .../corda/behave/scenarios/steps/CashSteps.kt | 41 +++++--- .../scenarios/steps/ConfigurationSteps.kt | 97 +++++++++++-------- .../behave/scenarios/steps/DatabaseSteps.kt | 17 ++-- .../behave/scenarios/steps/NetworkSteps.kt | 17 +++- .../corda/behave/scenarios/steps/RpcSteps.kt | 14 +-- .../corda/behave/scenarios/steps/SshSteps.kt | 17 ++-- .../behave/scenarios/steps/StartupSteps.kt | 83 +++++++++++----- .../behave/scenarios/steps/VaultSteps.kt | 44 +++++++++ .../scenarios/tests/StepsProviderTests.kt | 32 ++++++ .../features/cash/currencies.feature | 20 ++-- .../features/database/connection.feature | 10 +- .../features/startup/logging.feature | 18 ++-- .../behave/monitoring/MonitoringTests.kt | 36 +++---- .../net/corda/behave/network/NetworkTests.kt | 2 +- .../net/corda/behave/process/CommandTests.kt | 3 +- ...rviceTests.kt => PostreSQLServiceTests.kt} | 8 +- 54 files changed, 820 insertions(+), 398 deletions(-) delete mode 100644 experimental/behave/deps/corda/3.0.0/.gitkeep delete mode 100644 experimental/behave/deps/corda/3.0.0/apps/.gitkeep rename experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/{SqlServerConfigurationTemplate.kt => PostgresConfigurationTemplate.kt} (67%) rename experimental/behave/src/main/kotlin/net/corda/behave/service/database/{SqlServerService.kt => PostgreSQLService.kt} (60%) delete mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioRunner.kt delete mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsBlock.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/api/StepsBlock.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/api/StepsProvider.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Vault.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/VaultSteps.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/tests/StepsProviderTests.kt rename experimental/behave/src/test/kotlin/net/corda/behave/service/{SqlServerServiceTests.kt => PostreSQLServiceTests.kt} (54%) 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() From ca55c80bfc5a41e7aadd3623cdddc975fbd85744 Mon Sep 17 00:00:00 2001 From: josecoll Date: Thu, 19 Apr 2018 13:48:13 +0100 Subject: [PATCH 3/7] Move kotlin utility helper out of public into internals. (#2979) --- .../src/main/kotlin/net/corda/core/internal/InternalUtils.kt | 5 +++++ core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt | 4 ---- .../kotlin/net/corda/behave/scenarios/steps/VaultSteps.kt | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) 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 2f3899293f..411bb95454 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -464,3 +464,8 @@ fun NotarisationRequest.generateSignature(serviceHub: ServiceHub): NotarisationR } val PublicKey.hash: SecureHash get() = encoded.sha256() + +/** + * 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/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index 183b276fb8..675745f759 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -130,7 +130,3 @@ fun Future.getOrThrow(timeout: Duration? = null): V = try { 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/src/scenario/kotlin/net/corda/behave/scenarios/steps/VaultSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/VaultSteps.kt index 4a2b7b3c3c..e5249a2bb2 100644 --- 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 @@ -4,7 +4,7 @@ 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.core.internal.sumByLong import net.corda.finance.contracts.asset.Cash class VaultSteps : StepsBlock { From 1c7def8e129c18c554fce155d212cb280d6ed7f2 Mon Sep 17 00:00:00 2001 From: josecoll Date: Fri, 20 Apr 2018 11:07:36 +0100 Subject: [PATCH 4/7] Fix streaming subscription bug. (#2983) --- .../src/main/kotlin/net/corda/behave/process/Command.kt | 4 +++- .../src/test/kotlin/net/corda/behave/process/CommandTests.kt | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) 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 5a8b071365..817d7e56a3 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 @@ -5,6 +5,7 @@ import net.corda.behave.file.currentDirectory import net.corda.behave.logging.getLogger import net.corda.behave.process.output.OutputListener import rx.Observable +import rx.Subscriber import java.io.Closeable import java.io.File import java.io.IOException @@ -143,8 +144,9 @@ open class Command( return exitCode } - fun use(action: (Command, Observable) -> Unit = { _, _ -> }): Int { + fun use(subscriber: Subscriber, action: (Command, Observable) -> Unit = { _, _ -> }): Int { try { + output.subscribe(subscriber) start() action(this, output) } finally { 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 7495a98c4d..0d560b4b74 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 @@ -21,8 +21,7 @@ class CommandTests { @Test fun `output stream for command can be observed`() { val subscriber = TestSubscriber() - val exitCode = Command(listOf("ls", "/")).use { _, output -> - output.subscribe(subscriber) + val exitCode = Command(listOf("ls", "/")).use(subscriber) { _, output -> subscriber.awaitTerminalEvent() subscriber.assertCompleted() subscriber.assertNoErrors() From 342ecc4eedd213a8156a75fc05d2c5ece248329e Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Fri, 20 Apr 2018 15:03:07 +0100 Subject: [PATCH 5/7] CORDA-1265: Upgrade to Kotlin language/API 1.2 (#2871) * Upgrade to Kotlin language/API 1.2 * Kotlin 1.2 annotates private Companion instances as "@Deprecated". --- .ci/api-current.txt | 4 ++-- build.gradle | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 23b5465f15..fac366c8fa 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -3214,7 +3214,7 @@ public static final class net.corda.core.transactions.FilteredTransaction$Compan @org.jetbrains.annotations.NotNull public final List inputsOfType(Class) public String toString() public final void verify() - public static final net.corda.core.transactions.LedgerTransaction$Companion Companion + @java.lang.Deprecated public static final net.corda.core.transactions.LedgerTransaction$Companion Companion ## public static final class net.corda.core.transactions.LedgerTransaction$InOutGroup extends java.lang.Object public (List, List, Object) @@ -3320,7 +3320,7 @@ public static final class net.corda.core.transactions.NotaryChangeWireTransactio @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction withAdditionalSignature(java.security.KeyPair, net.corda.core.crypto.SignatureMetadata) @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction withAdditionalSignature(net.corda.core.crypto.TransactionSignature) @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction withAdditionalSignatures(Iterable) - public static final net.corda.core.transactions.SignedTransaction$Companion Companion + @java.lang.Deprecated public static final net.corda.core.transactions.SignedTransaction$Companion Companion ## @net.corda.core.serialization.CordaSerializable public static final class net.corda.core.transactions.SignedTransaction$SignaturesMissingException extends java.security.SignatureException implements net.corda.core.CordaThrowable, net.corda.core.contracts.NamedByHash public (Set, List, net.corda.core.crypto.SecureHash) diff --git a/build.gradle b/build.gradle index d2d38f8fde..99667f4a73 100644 --- a/build.gradle +++ b/build.gradle @@ -165,8 +165,8 @@ allprojects { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { - languageVersion = "1.1" - apiVersion = "1.1" + languageVersion = "1.2" + apiVersion = "1.2" jvmTarget = "1.8" javaParameters = true // Useful for reflection. } From 247a97f1a50149d4f4b465fe60f87c4c8195347d Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Fri, 20 Apr 2018 16:56:27 +0100 Subject: [PATCH 6/7] Remove superfluous registerInitiatedFlow calls from tutorial and example tests (#2944) --- .../net/corda/core/flows/FlowsInJavaTest.java | 5 ++--- .../core/flows/CollectSignaturesFlowTests.kt | 9 +-------- .../internal/ResolveTransactionsFlowTest.kt | 4 +--- docs/source/api-testing.rst | 18 ------------------ .../mocknetwork/TutorialMockNetwork.kt | 4 ++-- docs/source/flow-testing.rst | 4 ---- .../net/corda/traderdemo/TraderDemoTest.kt | 1 - 7 files changed, 6 insertions(+), 39 deletions(-) diff --git a/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java b/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java index f98b11141d..aa616b3ca0 100644 --- a/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java +++ b/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java @@ -1,6 +1,7 @@ package net.corda.core.flows; import co.paralleluniverse.fibers.Suspendable; +import com.google.common.collect.ImmutableList; import com.google.common.primitives.Primitives; import net.corda.core.identity.Party; import net.corda.testing.core.TestConstants; @@ -13,13 +14,12 @@ import org.junit.Test; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; -import static java.util.Collections.emptyList; import static net.corda.testing.core.TestUtils.singleIdentity; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.Assert.fail; public class FlowsInJavaTest { - private final MockNetwork mockNet = new MockNetwork(emptyList()); + private final MockNetwork mockNet = new MockNetwork(ImmutableList.of("net.corda.core.flows")); private StartedMockNode aliceNode; private StartedMockNode bobNode; private Party bob; @@ -38,7 +38,6 @@ public class FlowsInJavaTest { @Test public void suspendableActionInsideUnwrap() throws Exception { - bobNode.registerInitiatedFlow(SendHelloAndThenReceive.class); Future result = aliceNode.startFlow(new SendInUnwrapFlow(bob)); mockNet.runNetwork(); assertThat(result.get()).isEqualTo("Hello"); diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 9e862af098..b8f0275acb 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -41,7 +41,7 @@ class CollectSignaturesFlowTests { @Before fun setup() { - mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) + mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.core.flows")) aliceNode = mockNet.createPartyNode(ALICE_NAME) bobNode = mockNet.createPartyNode(BOB_NAME) charlieNode = mockNet.createPartyNode(CHARLIE_NAME) @@ -56,12 +56,6 @@ class CollectSignaturesFlowTests { mockNet.stopNodes() } - private fun registerFlowOnAllNodes(flowClass: KClass>) { - listOf(aliceNode, bobNode, charlieNode).forEach { - it.registerInitiatedFlow(flowClass.java) - } - } - // With this flow, the initiator starts the "CollectTransactionFlow". It is then the responders responsibility to // override "checkTransaction" and add whatever logic their require to verify the SignedTransaction they are // receiving off the wire. @@ -110,7 +104,6 @@ class CollectSignaturesFlowTests { // Normally this is handled by TransactionKeyFlow, but here we have to manually let A know about the identity aliceNode.services.identityService.verifyAndRegisterIdentity(bConfidentialIdentity) } - registerFlowOnAllNodes(TestFlow.Responder::class) val magicNumber = 1337 val parties = listOf(alice, bConfidentialIdentity.party, charlie) val state = DummyContract.MultiOwnerState(magicNumber, parties) diff --git a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt index b340601f12..52c356b102 100644 --- a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt @@ -38,12 +38,10 @@ class ResolveTransactionsFlowTest { @Before fun setup() { - mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) + mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.core.internal")) notaryNode = mockNet.defaultNotaryNode megaCorpNode = mockNet.createPartyNode(CordaX500Name("MegaCorp", "London", "GB")) miniCorpNode = mockNet.createPartyNode(CordaX500Name("MiniCorp", "London", "GB")) - megaCorpNode.registerInitiatedFlow(TestResponseFlow::class.java) - miniCorpNode.registerInitiatedFlow(TestResponseFlow::class.java) notary = mockNet.defaultNotaryIdentity megaCorp = megaCorpNode.info.singleIdentity() miniCorp = miniCorpNode.info.singleIdentity() diff --git a/docs/source/api-testing.rst b/docs/source/api-testing.rst index 6a04804b88..b2662c1acf 100644 --- a/docs/source/api-testing.rst +++ b/docs/source/api-testing.rst @@ -165,24 +165,6 @@ Nodes are created on the ``MockNetwork`` using: } } -Registering a node's initiated flows -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Regular Corda nodes automatically register any response flows defined in their installed CorDapps. When using a -``MockNetwork``, each ``StartedMockNode`` must manually register any responder flows it wishes to use. - -Responder flows are registered as follows: - -.. container:: codeset - - .. sourcecode:: kotlin - - nodeA.registerInitiatedFlow(ExampleFlow.Acceptor::class.java) - - .. sourcecode:: java - - nodeA.registerInitiatedFlow(ExampleFlow.Acceptor.class); - Running the network ^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/mocknetwork/TutorialMockNetwork.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/mocknetwork/TutorialMockNetwork.kt index 80a42a12a2..f62afd4c9c 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/mocknetwork/TutorialMockNetwork.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/mocknetwork/TutorialMockNetwork.kt @@ -1,6 +1,7 @@ package net.corda.docs.tutorial.mocknetwork import co.paralleluniverse.fibers.Suspendable +import com.google.common.collect.ImmutableList import net.corda.core.contracts.requireThat import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession @@ -62,10 +63,9 @@ class TutorialMockNetwork { @Before fun setUp() { - mockNet = MockNetwork(emptyList()) + mockNet = MockNetwork(ImmutableList.of("net.corda.docs.tutorial.mocknetwork")) nodeA = mockNet.createPartyNode() nodeB = mockNet.createPartyNode() - nodeB.registerInitiatedFlow(FlowB::class.java) } @After diff --git a/docs/source/flow-testing.rst b/docs/source/flow-testing.rst index df7844dd44..9d36351c63 100644 --- a/docs/source/flow-testing.rst +++ b/docs/source/flow-testing.rst @@ -61,10 +61,6 @@ transactions are valid) inside a ``database.transaction``. All node flows run w nodes themselves, but any time we need to use the database directly from a unit test, you need to provide a database transaction as shown here. -With regards to initiated flows (see :doc:`flow-state-machines` for information on initiated and initiating flows), the -full node automatically registers them by scanning the CorDapp jars. In a unit test environment this is not possible so -``MockNode`` has the ``registerInitiatedFlow`` method to manually register an initiated flow. - .. MockNetwork message manipulation .. -------------------------------- .. The MockNetwork has the ability to manipulate message streams. You can use this to test your flows behaviour on corrupted, diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index 938e4243f5..58e5dac502 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -39,7 +39,6 @@ class TraderDemoTest { startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(demoUser)), startNode(providedName = BOC_NAME, rpcUsers = listOf(bankUser)) ).map { (it.getOrThrow() as InProcess) } - nodeA.registerInitiatedFlow(BuyerFlow::class.java) val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map { val client = CordaRPCClient(it.rpcAddress) client.start(demoUser.username, demoUser.password).proxy From bf4d8ba08c79dff8abea8bb727dac6a3361932a4 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Mon, 23 Apr 2018 09:17:43 +0100 Subject: [PATCH 7/7] CORDA-1335: Scan attachment Jar only to speed-up the process. (#2982) * CORDA-1335: Scan attachment Jar only to speed-up the process. * CORDA-1335: Explicitly mention other types of contracts to scan. * CORDA-1335: Refactor to eliminate listing different subclasses of Contract in two separate places. --- .../nodeapi/internal/ClassloaderUtils.kt | 27 ++++++++++++++----- .../node/internal/cordapp/CordappLoader.kt | 9 ++----- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ClassloaderUtils.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ClassloaderUtils.kt index b7972dc2ec..b8715d867d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ClassloaderUtils.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ClassloaderUtils.kt @@ -3,40 +3,53 @@ package net.corda.nodeapi.internal import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner import net.corda.core.contracts.Contract import net.corda.core.contracts.ContractClassName +import net.corda.core.contracts.UpgradedContract +import net.corda.core.contracts.UpgradedContractWithLegacyConstraint import net.corda.core.internal.copyTo import net.corda.core.internal.deleteIfExists +import net.corda.core.internal.logElapsedTime import net.corda.core.internal.read +import org.slf4j.LoggerFactory import java.io.InputStream import java.lang.reflect.Modifier import java.net.URLClassLoader import java.nio.file.Files import java.nio.file.Path -import java.nio.file.Paths import java.nio.file.StandardCopyOption +import java.util.Collections.singleton + +// When scanning of the CorDapp Jar is performed without "corda-core.jar" being the in the classpath, there is no way to appreciate +// relationships between those interfaces, therefore they have to be listed explicitly. +val coreContractClasses = setOf(Contract::class, UpgradedContractWithLegacyConstraint::class, UpgradedContract::class) /** * Scans the jar for contracts. * @returns: found contract class names or null if none found */ fun scanJarForContracts(cordappJar: Path): List { - val currentClassLoader = Contract::class.java.classLoader val scanResult = FastClasspathScanner() - .addClassLoader(currentClassLoader) - .overrideClasspath(cordappJar, Paths.get(Contract::class.java.protectionDomain.codeSource.location.toURI())) + // A set of a single element may look odd, but if this is removed "Path" which itself is an `Iterable` + // is getting broken into pieces to scan individually, which doesn't yield desired effect. + .overrideClasspath(singleton(cordappJar)) .scan() - val contracts = (scanResult.getNamesOfClassesImplementing(Contract::class.qualifiedName) ).distinct() + val contracts = coreContractClasses.flatMap { contractClass -> scanResult.getNamesOfClassesImplementing(contractClass.qualifiedName) }.distinct() // Only keep instantiable contracts - return URLClassLoader(arrayOf(cordappJar.toUri().toURL()), currentClassLoader).use { + return URLClassLoader(arrayOf(cordappJar.toUri().toURL()), Contract::class.java.classLoader).use { contracts.map(it::loadClass).filter { !it.isInterface && !Modifier.isAbstract(it.modifiers) } }.map { it.name } } +private val logger = LoggerFactory.getLogger("ClassloaderUtils") + fun withContractsInJar(jarInputStream: InputStream, withContracts: (List, InputStream) -> T): T { val tempFile = Files.createTempFile("attachment", ".jar") try { jarInputStream.copyTo(tempFile, StandardCopyOption.REPLACE_EXISTING) - val contracts = scanJarForContracts(tempFile.toAbsolutePath()) + val cordappJar = tempFile.toAbsolutePath() + val contracts = logElapsedTime("Contracts loading for '$cordappJar'", logger) { + scanJarForContracts(cordappJar) + } return tempFile.read { withContracts(contracts, it) } } finally { tempFile.deleteIfExists() diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt index 5214ef0fb2..5cfd21b920 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt @@ -17,6 +17,7 @@ import net.corda.core.serialization.SerializeAsToken import net.corda.core.utilities.contextLogger import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.services.config.NodeConfiguration +import net.corda.nodeapi.internal.coreContractClasses import net.corda.nodeapi.internal.serialization.DefaultWhitelist import org.apache.commons.collections4.map.LRUMap import java.io.File @@ -241,13 +242,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List { - return (scanResult.getNamesOfClassesImplementing(Contract::class) + - scanResult.getNamesOfClassesImplementing(UpgradedContract::class) + - // Even though UpgradedContractWithLegacyConstraint implements UpgradedContract - // we need to specify it separately. Otherwise, classes implementing UpgradedContractWithLegacyConstraint - // don't get picked up. - scanResult.getNamesOfClassesImplementing(UpgradedContractWithLegacyConstraint::class)) - .distinct() + return coreContractClasses.flatMap { scanResult.getNamesOfClassesImplementing(it) }.distinct() } private fun findPlugins(cordappJarPath: RestrictedURL): List {