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.
This commit is contained in:
josecoll 2018-04-19 09:56:16 +01:00 committed by GitHub
parent a684507553
commit ec70478d70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 820 additions and 398 deletions

View File

@ -129,3 +129,8 @@ fun <V> Future<V>.getOrThrow(timeout: Duration? = null): V = try {
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
throw e.cause!! throw e.cause!!
} }
/**
* Extension method for providing a sumBy method that processes and returns a Long
*/
fun <T> Iterable<T>.sumByLong(selector: (T) -> Long): Long = this.map { selector(it) }.sum()

View File

@ -5,7 +5,7 @@ and test homogeneous and heterogeneous Corda networks on a local
machine. The framework has built-in support for Dockerised node machine. The framework has built-in support for Dockerised node
dependencies so that you easily can spin up a Corda node locally dependencies so that you easily can spin up a Corda node locally
that, for instance, uses a 3rd party database provider such as that, for instance, uses a 3rd party database provider such as
MS SQL Server or Postgres. Postgres.
# Structure # Structure

View File

@ -66,6 +66,12 @@ dependencies {
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
compile "org.apache.logging.log4j:log4j-core:$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-io:commons-io:$commonsio_version"
compile "commons-logging:commons-logging:$commonslogging_version" compile "commons-logging:commons-logging:$commonslogging_version"
compile "com.spotify:docker-client:$docker_client_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-java8:$cucumber_version"
scenarioCompile "info.cukes:cucumber-junit:$cucumber_version" scenarioCompile "info.cukes:cucumber-junit:$cucumber_version"
scenarioCompile "info.cukes:cucumber-picocontainer:$cucumber_version" scenarioCompile "info.cukes:cucumber-picocontainer:$cucumber_version"
} }
compileKotlin { compileKotlin {

View File

@ -1,3 +1,4 @@
Download and store database drivers here; for example: Download and store database drivers here; for example:
- h2-1.4.196.jar - h2-1.4.196.jar
- mssql-jdbc-6.2.2.jre8.jar - postgresql-42.1.4.jar

View File

@ -1,24 +1,31 @@
#!/bin/bash #!/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 # Set up directories
mkdir -p deps/corda/${VERSION}/apps mkdir -p ${STAGING_DIR}/apps
mkdir -p deps/drivers mkdir -p ${DRIVERS_DIR}
# Copy Corda capsule into deps # 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 # 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 "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 "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 -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 # Build Network Bootstrapper
cd ../..
./gradlew buildBootstrapperJar ./gradlew buildBootstrapperJar
./gradlew :finance:jar cp -v $(ls tools/bootstrapper/build/libs/*.jar | tail -n1) experimental/behave/${STAGING_DIR}/network-bootstrapper.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

View File

@ -16,6 +16,9 @@ class DatabaseSettings {
var userName: String = "sa" var userName: String = "sa"
private set private set
var driverJar: String? = null
private set
private var databaseConfigTemplate: DatabaseConfigurationTemplate = DatabaseConfigurationTemplate() private var databaseConfigTemplate: DatabaseConfigurationTemplate = DatabaseConfigurationTemplate()
private val serviceInitiators = mutableListOf<ServiceInitiator>() private val serviceInitiators = mutableListOf<ServiceInitiator>()
@ -30,6 +33,11 @@ class DatabaseSettings {
return this return this
} }
fun withDriver(name: String): DatabaseSettings {
driverJar = name
return this
}
fun withUser(name: String): DatabaseSettings { fun withUser(name: String): DatabaseSettings {
userName = name userName = name
return this return this

View File

@ -1,11 +1,11 @@
package net.corda.behave.database package net.corda.behave.database
import net.corda.behave.database.configuration.H2ConfigurationTemplate 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.Configuration
import net.corda.behave.node.configuration.DatabaseConfiguration import net.corda.behave.node.configuration.DatabaseConfiguration
import net.corda.behave.service.database.H2Service 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) { enum class DatabaseType(val settings: DatabaseSettings) {
@ -19,16 +19,19 @@ enum class DatabaseType(val settings: DatabaseSettings) {
} }
), ),
SQL_SERVER(DatabaseSettings() POSTGRES(DatabaseSettings()
.withDatabase(SqlServerService.database) .withDatabase(PostgreSQLService.database)
.withSchema(SqlServerService.schema) .withDriver(PostgreSQLService.driver)
.withUser(SqlServerService.username) .withSchema(PostgreSQLService.schema)
.withConfigTemplate(SqlServerConfigurationTemplate()) .withUser(PostgreSQLService.username)
.withConfigTemplate(PostgresConfigurationTemplate())
.withServiceInitiator { .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 dependencies(config: Configuration) = settings.dependencies(config)
fun connection(config: DatabaseConfiguration) = DatabaseConnection(config, settings.template) 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()) { fun fromName(name: String): DatabaseType? = when (name.toLowerCase()) {
"h2" -> H2 "h2" -> H2
"sql_server" -> SQL_SERVER "postgres" -> POSTGRES
"sql server" -> SQL_SERVER "postgresql" -> POSTGRES
"sqlserver" -> SQL_SERVER
else -> null else -> null
} }

View File

@ -3,26 +3,23 @@ package net.corda.behave.database.configuration
import net.corda.behave.database.DatabaseConfigurationTemplate import net.corda.behave.database.DatabaseConfigurationTemplate
import net.corda.behave.node.configuration.DatabaseConfiguration import net.corda.behave.node.configuration.DatabaseConfiguration
class SqlServerConfigurationTemplate : DatabaseConfigurationTemplate() { class PostgresConfigurationTemplate : DatabaseConfigurationTemplate() {
override val connectionString: (DatabaseConfiguration) -> String 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 override val config: (DatabaseConfiguration) -> String
get() = { get() = {
""" """
|dataSourceProperties = { |dataSourceProperties = {
| dataSourceClassName = "com.microsoft.sqlserver.jdbc.SQLServerDataSource" | dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource"
| dataSource.url = "${connectionString(it)}" | dataSource.url = "${connectionString(it)}"
| dataSource.user = "${it.username}" | dataSource.user = "${it.username}"
| dataSource.password = "${it.password}" | dataSource.password = "${it.password}"
|} |}
|database = { |database = {
| initialiseSchema=true
| transactionIsolationLevel = READ_COMMITTED | transactionIsolationLevel = READ_COMMITTED
| schema="${it.schema}"
|} |}
""" """
} }
} }

View File

@ -5,4 +5,10 @@ import java.io.File
val currentDirectory: File val currentDirectory: File
get() = File(System.getProperty("user.dir")) 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) operator fun File.div(relative: String): File = this.resolve(relative)

View File

@ -1,23 +1,24 @@
package net.corda.behave.monitoring package net.corda.behave.monitoring
import net.corda.behave.await import net.corda.behave.await
import rx.Observable
import java.time.Duration import java.time.Duration
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
class ConjunctiveWatch( class ConjunctiveWatch(
private val left: Watch, private val left: Watch,
private val right: Watch private val right: Watch
) : Watch() { ) : Watch {
override fun await(observable: Observable<String>, timeout: Duration): Boolean { override fun ready() = left.ready() && right.ready()
val latch = CountDownLatch(2)
override fun await(timeout: Duration): Boolean {
val countDownLatch = CountDownLatch(2)
listOf(left, right).parallelStream().forEach { listOf(left, right).parallelStream().forEach {
if (it.await(observable, timeout)) { if (it.await(timeout)) {
latch.countDown() countDownLatch.countDown()
} }
} }
return latch.await(timeout) return countDownLatch.await(timeout)
} }
} }

View File

@ -8,16 +8,18 @@ import java.util.concurrent.CountDownLatch
class DisjunctiveWatch( class DisjunctiveWatch(
private val left: Watch, private val left: Watch,
private val right: Watch private val right: Watch
) : Watch() { ) : Watch {
override fun await(observable: Observable<String>, timeout: Duration): Boolean { override fun ready() = left.ready() || right.ready()
val latch = CountDownLatch(1)
override fun await(timeout: Duration): Boolean {
val countDownLatch = CountDownLatch(1)
listOf(left, right).parallelStream().forEach { listOf(left, right).parallelStream().forEach {
if (it.await(observable, timeout)) { if (it.await(timeout)) {
latch.countDown() countDownLatch.countDown()
} }
} }
return latch.await(timeout) return countDownLatch.await(timeout)
} }
} }

View File

@ -1,22 +1,24 @@
package net.corda.behave.monitoring package net.corda.behave.monitoring
import rx.Observable
class PatternWatch( class PatternWatch(
observable: Observable<String>,
pattern: String, pattern: String,
ignoreCase: Boolean = false ignoreCase: Boolean = false
) : Watch() { ) : AbstractWatch<String>(observable, false) {
private val regularExpression = if (ignoreCase) { private val regularExpression: Regex = if (ignoreCase) {
Regex("^.*$pattern.*$", RegexOption.IGNORE_CASE) Regex("^.*$pattern.*$", RegexOption.IGNORE_CASE)
} else { } else {
Regex("^.*$pattern.*$") Regex("^.*$pattern.*$")
} }
override fun match(data: String) = regularExpression.matches(data.trim()) init {
run()
companion object {
val EMPTY = PatternWatch("")
} }
override fun match(data: String): Boolean {
return regularExpression.matches(data.trim())
}
} }

View File

@ -6,28 +6,46 @@ import rx.Observable
import java.time.Duration import java.time.Duration
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
abstract class Watch { interface Watch {
fun await(timeout: Duration = 10.seconds): Boolean
private val latch = CountDownLatch(1) fun ready(): Boolean
open fun await(
observable: Observable<String>,
timeout: Duration = 10.seconds
): Boolean {
observable
.filter { match(it) }
.forEach { latch.countDown() }
return latch.await(timeout)
}
open fun match(data: String): Boolean = false
operator fun times(other: Watch): Watch { operator fun times(other: Watch): Watch {
return ConjunctiveWatch(this, other) return ConjunctiveWatch(this, other)
} }
operator fun div(other: Watch): Watch { operator fun div(other: Watch): Watch {
return DisjunctiveWatch(this, other) 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<T>(val observable: Observable<T>, 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
} }

View File

@ -4,12 +4,14 @@ import net.corda.behave.database.DatabaseType
import net.corda.behave.file.LogSource import net.corda.behave.file.LogSource
import net.corda.behave.file.currentDirectory import net.corda.behave.file.currentDirectory
import net.corda.behave.file.div import net.corda.behave.file.div
import net.corda.behave.file.stagingRoot
import net.corda.behave.logging.getLogger import net.corda.behave.logging.getLogger
import net.corda.behave.minutes import net.corda.behave.minutes
import net.corda.behave.node.Distribution import net.corda.behave.node.Distribution
import net.corda.behave.node.Node import net.corda.behave.node.Node
import net.corda.behave.node.configuration.NotaryType import net.corda.behave.node.configuration.NotaryType
import net.corda.behave.process.JarCommand import net.corda.behave.process.JarCommand
import net.corda.core.CordaException
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
@ -26,8 +28,6 @@ class Network private constructor(
private val timeout: Duration = 2.minutes private val timeout: Duration = 2.minutes
) : Closeable, Iterable<Node> { ) : Closeable, Iterable<Node> {
private val log = getLogger<Network>()
private val latch = CountDownLatch(1) private val latch = CountDownLatch(1)
private var isRunning = false private var isRunning = false
@ -36,6 +36,10 @@ class Network private constructor(
private var hasError = false private var hasError = false
init {
FileUtils.forceMkdir(targetDirectory)
}
class Builder internal constructor( class Builder internal constructor(
private val timeout: Duration private val timeout: Duration
) { ) {
@ -43,7 +47,7 @@ class Network private constructor(
private val nodes = mutableMapOf<String, Node>() private val nodes = mutableMapOf<String, Node>()
private val startTime = DateTimeFormatter private val startTime = DateTimeFormatter
.ofPattern("yyyyMMDD-HHmmss") .ofPattern("yyyyMMdd-HHmmss")
.withZone(ZoneId.of("UTC")) .withZone(ZoneId.of("UTC"))
.format(Instant.now()) .format(Instant.now())
@ -51,7 +55,7 @@ class Network private constructor(
fun addNode( fun addNode(
name: String, name: String,
distribution: Distribution = Distribution.LATEST_MASTER, distribution: Distribution = Distribution.MASTER,
databaseType: DatabaseType = DatabaseType.H2, databaseType: DatabaseType = DatabaseType.H2,
notaryType: NotaryType = NotaryType.NONE, notaryType: NotaryType = NotaryType.NONE,
issuableCurrencies: List<String> = emptyList() issuableCurrencies: List<String> = emptyList()
@ -76,22 +80,28 @@ class Network private constructor(
fun generate(): Network { fun generate(): Network {
val network = Network(nodes, directory, timeout) 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 return network
} }
} }
private fun copyDatabaseDrivers() { fun copyDatabaseDrivers() {
val driverDirectory = targetDirectory / "libs" val driverDirectory = targetDirectory / "libs"
log.info("Copying database drivers from $stagingRoot/deps/drivers to $driverDirectory")
FileUtils.forceMkdir(driverDirectory) FileUtils.forceMkdir(driverDirectory)
FileUtils.copyDirectory( FileUtils.copyDirectory(
currentDirectory / "deps/drivers", stagingRoot / "deps/drivers",
driverDirectory driverDirectory
) )
} }
private fun configureNodes(): Boolean { fun configureNodes(): Boolean {
var allDependenciesStarted = true var allDependenciesStarted = true
log.info("Configuring nodes ...") log.info("Configuring nodes ...")
for (node in nodes.values) { for (node in nodes.values) {
@ -109,19 +119,14 @@ class Network private constructor(
} }
} }
private fun bootstrapNetwork() { private fun bootstrapLocalNetwork() {
copyDatabaseDrivers()
if (!configureNodes()) {
hasError = true
return
}
val bootstrapper = nodes.values val bootstrapper = nodes.values
.sortedByDescending { it.config.distribution.version } .sortedByDescending { it.config.distribution.version }
.first() .first()
.config.distribution.networkBootstrapper .config.distribution.networkBootstrapper
if (!bootstrapper.exists()) { if (!bootstrapper.exists()) {
log.warn("Network bootstrapping tool does not exist; continuing ...") signalFailure("Network bootstrapping tool does not exist; exiting ...")
return return
} }
@ -205,11 +210,15 @@ class Network private constructor(
} }
isRunning = true isRunning = true
for (node in nodes.values) { for (node in nodes.values) {
log.info("Starting node [{}]", node.config.name)
node.start() node.start()
} }
} }
fun waitUntilRunning(waitDuration: Duration? = null): Boolean { fun waitUntilRunning(waitDuration: Duration? = null): Boolean {
log.info("Network.waitUntilRunning")
if (hasError) { if (hasError) {
return false return false
} }
@ -264,6 +273,7 @@ class Network private constructor(
} }
log.info("Shutting down network ...") log.info("Shutting down network ...")
isStopped = true isStopped = true
log.info("Shutting down nodes ...")
for (node in nodes.values) { for (node in nodes.values) {
node.shutDown() node.shutDown()
} }
@ -289,13 +299,10 @@ class Network private constructor(
} }
companion object { companion object {
val log = getLogger<Network>()
const val CLEANUP_ON_ERROR = false const val CLEANUP_ON_ERROR = false
fun new( fun new(timeout: Duration = 2.minutes
timeout: Duration = 2.minutes
): Builder = Builder(timeout) ): Builder = Builder(timeout)
} }
} }

View File

@ -1,6 +1,9 @@
package net.corda.behave.node package net.corda.behave.node
import net.corda.behave.file.div 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 org.apache.commons.io.FileUtils
import java.io.File import java.io.File
import java.net.URL import java.net.URL
@ -23,39 +26,53 @@ class Distribution private constructor(
/** /**
* The URL of the distribution fat JAR, if available. * 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. * 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. * 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. * 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. * Ensure that the distribution is available on disk.
*/ */
fun ensureAvailable() { fun ensureAvailable() {
if (!jarFile.exists()) { if (!cordaJar.exists()) {
if (url != null) { if (url != null) {
try { try {
FileUtils.forceMkdirParent(jarFile) FileUtils.forceMkdirParent(cordaJar)
FileUtils.copyURLToFile(url, jarFile) FileUtils.copyURLToFile(url, cordaJar)
} catch (e: Exception) { } 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 { } 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. * Human-readable representation of the distribution.
*/ */
override fun toString() = "Corda(version = $version, path = $jarFile)" override fun toString() = "Corda(version = $version, path = $cordaJar)"
companion object { companion object {
protected val log = getLogger<Service>()
private val distributions = mutableListOf<Distribution>() private val distributions = mutableListOf<Distribution>()
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") fun fromArtifactory(version: String): Distribution {
val url = URL("https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases/net/corda/corda/$version/corda-$version.jar")
val LATEST_MASTER = V3 log.info("Artifactory URL: $url\n")
/**
* 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")
val distribution = Distribution(version, url = url) val distribution = Distribution(version, url = url)
distributions.add(distribution) distributions.add(distribution)
return distribution return distribution
@ -102,15 +115,25 @@ class Distribution private constructor(
return distribution 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. * Get registered representation of a Corda distribution based on its version string.
* @param version The version of the Corda distribution * @param version The version of the Corda distribution
*/ */
fun fromVersionString(version: String): Distribution? = when (version.toLowerCase()) { fun fromVersionString(version: String): Distribution = when (version) {
"master" -> LATEST_MASTER "master" -> MASTER
else -> distributions.firstOrNull { it.version == version } "corda-3.0" -> fromArtifactory(version)
else -> fromJarFile(version)
} }
} }
} }

View File

@ -5,6 +5,7 @@ import net.corda.behave.database.DatabaseType
import net.corda.behave.file.LogSource import net.corda.behave.file.LogSource
import net.corda.behave.file.currentDirectory import net.corda.behave.file.currentDirectory
import net.corda.behave.file.div import net.corda.behave.file.div
import net.corda.behave.file.stagingRoot
import net.corda.behave.logging.getLogger import net.corda.behave.logging.getLogger
import net.corda.behave.monitoring.PatternWatch import net.corda.behave.monitoring.PatternWatch
import net.corda.behave.node.configuration.* import net.corda.behave.node.configuration.*
@ -20,6 +21,7 @@ import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import java.io.File import java.io.File
import java.net.InetAddress
import java.time.Duration import java.time.Duration
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
@ -39,14 +41,14 @@ class Node(
private val logDirectory = runtimeDirectory / "logs" private val logDirectory = runtimeDirectory / "logs"
private val command = JarCommand( private val command = JarCommand(
config.distribution.jarFile, config.distribution.cordaJar,
arrayOf("--config", "node.conf"), arrayOf("--config", "node.conf"),
runtimeDirectory, runtimeDirectory,
settings.timeout, settings.timeout,
enableRemoteDebugging = false 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 private var isConfigured = false
@ -76,7 +78,7 @@ class Node(
log.info("Configuring {} ...", this) log.info("Configuring {} ...", this)
serviceDependencies.addAll(config.database.type.dependencies(config)) serviceDependencies.addAll(config.database.type.dependencies(config))
config.distribution.ensureAvailable() config.distribution.ensureAvailable()
config.writeToFile(rootDirectory / "${config.name}.conf") config.writeToFile(rootDirectory / "${config.name}_node.conf")
installApps() installApps()
} }
@ -97,7 +99,7 @@ class Node(
} }
fun waitUntilRunning(waitDuration: Duration? = null): Boolean { fun waitUntilRunning(waitDuration: Duration? = null): Boolean {
val ok = isAliveLatch.await(command.output, waitDuration ?: settings.timeout) val ok = isAliveLatch.await(waitDuration ?: settings.timeout)
if (!ok) { if (!ok) {
log.warn("{} did not start up as expected within the given time frame", this) log.warn("{} did not start up as expected within the given time frame", this)
} else { } else {
@ -126,7 +128,8 @@ class Node(
} }
val logOutput: LogSource by lazy { 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 { val database: DatabaseConnection by lazy {
@ -216,7 +219,7 @@ class Node(
private fun installApps() { private fun installApps() {
val version = config.distribution.version val version = config.distribution.version
val appDirectory = rootDirectory / "../../../deps/corda/$version/apps" val appDirectory = stagingRoot / "deps/corda/$version/apps"
if (appDirectory.exists()) { if (appDirectory.exists()) {
val targetAppDirectory = runtimeDirectory / "cordapps" val targetAppDirectory = runtimeDirectory / "cordapps"
FileUtils.copyDirectory(appDirectory, targetAppDirectory) FileUtils.copyDirectory(appDirectory, targetAppDirectory)
@ -228,7 +231,7 @@ class Node(
var name: String? = null var name: String? = null
private set private set
private var distribution = Distribution.V3 private var distribution = Distribution.MASTER
private var databaseType = DatabaseType.H2 private var databaseType = DatabaseType.H2
@ -314,13 +317,14 @@ class Node(
databaseType, databaseType,
location = location, location = location,
country = country, country = country,
notary = NotaryConfiguration(notaryType),
cordapps = CordappConfiguration(
apps = apps,
includeFinance = includeFinance
),
configElements = *arrayOf( configElements = *arrayOf(
NotaryConfiguration(notaryType), NotaryConfiguration(notaryType),
CurrencyConfiguration(issuableCurrencies), CurrencyConfiguration(issuableCurrencies)
CordappConfiguration(
apps = *apps.toTypedArray(),
includeFinance = includeFinance
)
) )
), ),
directory, directory,
@ -331,7 +335,6 @@ class Node(
private fun <T> error(message: String): T { private fun <T> error(message: String): T {
throw IllegalArgumentException(message) throw IllegalArgumentException(message)
} }
} }
companion object { companion object {

View File

@ -1,13 +1,15 @@
package net.corda.behave.node.configuration package net.corda.behave.node.configuration
import net.corda.behave.database.DatabaseType import net.corda.behave.database.DatabaseType
import net.corda.behave.logging.getLogger
import net.corda.behave.node.* import net.corda.behave.node.*
import net.corda.core.identity.CordaX500Name
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import java.io.File import java.io.File
class Configuration( class Configuration(
val name: String, val name: String,
val distribution: Distribution = Distribution.LATEST_MASTER, val distribution: Distribution = Distribution.MASTER,
val databaseType: DatabaseType = DatabaseType.H2, val databaseType: DatabaseType = DatabaseType.H2,
val location: String = "London", val location: String = "London",
val country: String = "GB", val country: String = "GB",
@ -19,19 +21,21 @@ class Configuration(
nodeInterface.dbPort, nodeInterface.dbPort,
password = DEFAULT_PASSWORD password = DEFAULT_PASSWORD
), ),
val notary: NotaryConfiguration = NotaryConfiguration(),
val cordapps: CordappConfiguration = CordappConfiguration(),
vararg configElements: ConfigurationTemplate vararg configElements: ConfigurationTemplate
) { ) {
private val developerMode = true private val developerMode = true
private val useHttps = false val cordaX500Name: CordaX500Name by lazy({
CordaX500Name(name, location, country)
})
private val basicConfig = """ private val basicConfig = """
|myLegalName="C=$country,L=$location,O=$name" |myLegalName="C=$country,L=$location,O=$name"
|keyStorePassword="cordacadevpass" |keyStorePassword="cordacadevpass"
|trustStorePassword="trustpass" |trustStorePassword="trustpass"
|extraAdvertisedServiceIds=[ "" ]
|useHTTPS=$useHttps
|devMode=$developerMode |devMode=$developerMode
|jarDirs = [ "../libs" ] |jarDirs = [ "../libs" ]
""".trimMargin() """.trimMargin()
@ -41,6 +45,7 @@ class Configuration(
fun writeToFile(file: File) { fun writeToFile(file: File) {
FileUtils.writeStringToFile(file, this.generate(), "UTF-8") FileUtils.writeStringToFile(file, this.generate(), "UTF-8")
log.info(this.generate())
} }
private fun generate() = listOf(basicConfig, database.config(), extraConfig) private fun generate() = listOf(basicConfig, database.config(), extraConfig)
@ -48,9 +53,8 @@ class Configuration(
.joinToString("\n") .joinToString("\n")
companion object { companion object {
private val log = getLogger<Configuration>()
private val DEFAULT_PASSWORD = "S0meS3cretW0rd" val DEFAULT_PASSWORD = "S0meS3cretW0rd"
} }
} }

View File

@ -1,8 +1,8 @@
package net.corda.behave.node.configuration package net.corda.behave.node.configuration
class CordappConfiguration(vararg apps: String, var includeFinance: Boolean = false) : ConfigurationTemplate() { class CordappConfiguration(var apps: List<String> = 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") listOf("net.corda:corda-finance:CORDA_VERSION")
} else { } else {
emptyList() emptyList()

View File

@ -8,9 +8,11 @@ class CurrencyConfiguration(private val issuableCurrencies: List<String>) : Conf
"" ""
} else { } else {
""" """
|issuableCurrencies=[ |custom : {
| ${issuableCurrencies.joinToString(", ")} | issuableCurrencies : [
|] | ${issuableCurrencies.joinToString(", ")}
| ]
|}
""" """
} }
} }

View File

@ -10,7 +10,8 @@ data class NetworkInterface(
val rpcPort: Int = getPort(12002 + (nodeIndex * 5)), val rpcPort: Int = getPort(12002 + (nodeIndex * 5)),
val rpcAdminPort: Int = getPort(12003 + (nodeIndex * 5)), val rpcAdminPort: Int = getPort(12003 + (nodeIndex * 5)),
val webPort: Int = getPort(12004 + (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() { ) : ConfigurationTemplate() {
init { init {
@ -28,7 +29,6 @@ data class NetworkInterface(
| address = "$host:$rpcPort" | address = "$host:$rpcPort"
| adminAddress = "$host:$rpcAdminPort" | adminAddress = "$host:$rpcAdminPort"
|} |}
|webAddress="$host:$webPort"
""" """
} }

View File

@ -1,6 +1,6 @@
package net.corda.behave.node.configuration 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 override val config: (Configuration) -> String
get() = { get() = {

View File

@ -27,12 +27,12 @@ open class Command(
private var process: Process? = null private var process: Process? = null
private lateinit var outputListener: OutputListener private var outputListener: OutputListener? = null
var exitCode = -1 var exitCode = -1
private set private set
val output: Observable<String> = Observable.create<String> { emitter -> val output: Observable<String> = Observable.create<String>({ emitter ->
outputListener = object : OutputListener { outputListener = object : OutputListener {
override fun onNewLine(line: String) { override fun onNewLine(line: String) {
emitter.onNext(line) emitter.onNext(line)
@ -42,10 +42,11 @@ open class Command(
emitter.onCompleted() emitter.onCompleted()
} }
} }
} }).share()
private val thread = Thread(Runnable { private val thread = Thread(Runnable {
try { try {
log.info("Command: $command")
val processBuilder = ProcessBuilder(command) val processBuilder = ProcessBuilder(command)
.directory(directory) .directory(directory)
.redirectErrorStream(true) .redirectErrorStream(true)
@ -57,13 +58,17 @@ open class Command(
while (true) { while (true) {
try { try {
val line = input.readLine()?.trimEnd() ?: break val line = input.readLine()?.trimEnd() ?: break
outputListener.onNewLine(line) log.trace(line)
outputListener?.onNewLine(line)
} catch (_: IOException) { } catch (_: IOException) {
break break
} catch (ex: Exception) {
log.error("Unexpected exception during reading input", ex)
break
} }
} }
input.close() input.close()
outputListener.onEndOfStream() outputListener?.onEndOfStream()
outputCapturedLatch.countDown() outputCapturedLatch.countDown()
}).start() }).start()
val streamIsClosed = outputCapturedLatch.await(timeout) val streamIsClosed = outputCapturedLatch.await(timeout)
@ -88,13 +93,15 @@ open class Command(
} }
} catch (e: Exception) { } catch (e: Exception) {
log.warn("Error occurred when trying to run process", e) log.warn("Error occurred when trying to run process", e)
throw e
}
finally {
process = null
terminationLatch.countDown()
} }
process = null
terminationLatch.countDown()
}) })
fun start() { fun start() {
output.subscribe()
thread.start() thread.start()
} }

View File

@ -4,7 +4,7 @@ import java.io.File
import java.time.Duration import java.time.Duration
class JarCommand( class JarCommand(
jarFile: File, val jarFile: File,
arguments: Array<String>, arguments: Array<String>,
directory: File, directory: File,
timeout: Duration, timeout: Duration,

View File

@ -6,13 +6,13 @@ import com.spotify.docker.client.messages.ContainerConfig
import com.spotify.docker.client.messages.HostConfig import com.spotify.docker.client.messages.HostConfig
import com.spotify.docker.client.messages.PortBinding import com.spotify.docker.client.messages.PortBinding
import net.corda.behave.monitoring.PatternWatch import net.corda.behave.monitoring.PatternWatch
import net.corda.behave.monitoring.Watch
import rx.Observable import rx.Observable
import java.io.Closeable import java.io.Closeable
abstract class ContainerService( abstract class ContainerService(
name: String, name: String,
port: Int, port: Int,
val startupStatement: String,
settings: ServiceSettings = ServiceSettings() settings: ServiceSettings = ServiceSettings()
) : Service(name, port, settings), Closeable { ) : Service(name, port, settings), Closeable {
@ -30,8 +30,6 @@ abstract class ContainerService(
private val environmentVariables: MutableList<String> = mutableListOf() private val environmentVariables: MutableList<String> = mutableListOf()
private var startupStatement: Watch = PatternWatch.EMPTY
private val imageReference: String private val imageReference: String
get() = "$baseImage:$imageTag" get() = "$baseImage:$imageTag"
@ -51,7 +49,12 @@ abstract class ContainerService(
val creation = client.createContainer(containerConfig) val creation = client.createContainer(containerConfig)
id = creation.id() id = creation.id()
val info = client.inspectContainer(id)
log.info("Container $id info: $info")
client.startContainer(id) client.startContainer(id)
true true
} catch (e: Exception) { } catch (e: Exception) {
id = null id = null
@ -73,10 +76,6 @@ abstract class ContainerService(
environmentVariables.add("$name=$value") environmentVariables.add("$name=$value")
} }
protected fun setStartupStatement(statement: String) {
startupStatement = PatternWatch(statement)
}
override fun checkPrerequisites() { override fun checkPrerequisites() {
if (!client.listImages().any { true == it.repoTags()?.contains(imageReference) }) { if (!client.listImages().any { true == it.repoTags()?.contains(imageReference) }) {
log.info("Pulling image $imageReference ...") log.info("Pulling image $imageReference ...")
@ -97,8 +96,8 @@ abstract class ContainerService(
while (timeout > 0) { while (timeout > 0) {
client.logs(id, DockerClient.LogsParam.stdout(), DockerClient.LogsParam.stderr()).use { client.logs(id, DockerClient.LogsParam.stdout(), DockerClient.LogsParam.stderr()).use {
val contents = it.readFully() val contents = it.readFully()
val observable = Observable.from(contents.split("\n")) val observable = Observable.from(contents.split("\n", "\r"))
if (startupStatement.await(observable, settings.pollInterval)) { if (PatternWatch(observable, startupStatement).await(settings.pollInterval)) {
log.info("Found process start-up statement for {}", this) log.info("Found process start-up statement for {}", this)
return true return true
} }
@ -118,5 +117,4 @@ abstract class ContainerService(
client.close() client.close()
} }
} }
} }

View File

@ -2,31 +2,25 @@ package net.corda.behave.service.database
import net.corda.behave.database.DatabaseConnection import net.corda.behave.database.DatabaseConnection
import net.corda.behave.database.DatabaseType 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.node.configuration.DatabaseConfiguration
import net.corda.behave.service.ContainerService import net.corda.behave.service.ContainerService
import net.corda.behave.service.ServiceSettings import net.corda.behave.service.ServiceSettings
class SqlServerService( class PostgreSQLService(
name: String, name: String,
port: Int, port: Int,
private val password: String, private val password: String,
settings: ServiceSettings = ServiceSettings() 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 override val internalPort = 5432
init {
addEnvironmentVariable("ACCEPT_EULA", "Y")
addEnvironmentVariable("SA_PASSWORD", password)
setStartupStatement("SQL Server is now ready for client connections")
}
override fun verify(): Boolean { override fun verify(): Boolean {
val config = DatabaseConfiguration( val config = DatabaseConfiguration(
type = DatabaseType.SQL_SERVER, type = DatabaseType.POSTGRES,
host = host, host = host,
port = port, port = port,
database = database, database = database,
@ -34,7 +28,7 @@ class SqlServerService(
username = username, username = username,
password = password password = password
) )
val connection = DatabaseConnection(config, SqlServerConfigurationTemplate()) val connection = DatabaseConnection(config, PostgresConfigurationTemplate())
try { try {
connection.use { connection.use {
return true return true
@ -47,12 +41,11 @@ class SqlServerService(
} }
companion object { companion object {
val host = "localhost" val host = "localhost"
val database = "master" val database = "postgres"
val schema = "dbo" val schema = "public"
val username = "sa" val username = "postgres"
val driver = "postgresql-42.1.4.jar"
} }
} }

View File

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

View File

@ -0,0 +1,59 @@
@file:JvmName("ScenarioRunner")
package net.corda.behave.scenarios
import joptsimple.OptionParser
import kotlin.system.exitProcess
fun main(args: Array<out String>) {
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 @<tag-name>")
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 <location of feature scenario definitions>
Examples:
ScenarioRunner -path <features-dir>
ScenarioRunner -path <features-dir>/<name>.feature
ScenarioRunner -path <features-dir>/<name>.feature:3:9
ScenarioRunner -path <features-dir> --plugin html --tags @qa
ScenarioRunner -path <features-dir> --plugin html --tags @compatibility
Please refer to the Cucumber documentation https://cucumber.io/docs/reference/jvm for more info.
""".trimIndent())
parser.printHelpOn(System.out)
}

View File

@ -1,10 +1,12 @@
package net.corda.behave.scenarios package net.corda.behave.scenarios
import cucumber.api.java.After
import net.corda.behave.logging.getLogger import net.corda.behave.logging.getLogger
import net.corda.behave.network.Network import net.corda.behave.network.Network
import net.corda.behave.node.Node import net.corda.behave.node.Node
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import java.time.Duration
class ScenarioState { class ScenarioState {
@ -36,7 +38,7 @@ class ScenarioState {
return nodes.firstOrNull { it.name == nodeName(name) } ?: newNode(name) return nodes.firstOrNull { it.name == nodeName(name) } ?: newNode(name)
} }
fun ensureNetworkIsRunning() { fun ensureNetworkIsRunning(timeout: Duration? = null) {
if (network != null) { if (network != null) {
// Network is already running // Network is already running
return return
@ -47,7 +49,7 @@ class ScenarioState {
} }
network = networkBuilder.generate() network = networkBuilder.generate()
network?.start() network?.start()
assertThat(network?.waitUntilRunning()).isTrue() assertThat(network?.waitUntilRunning(timeout)).isTrue()
} }
inline fun <T> withNetwork(action: ScenarioState.() -> T): T { inline fun <T> withNetwork(action: ScenarioState.() -> T): T {
@ -63,6 +65,7 @@ class ScenarioState {
} }
} }
@After
fun stopNetwork() { fun stopNetwork() {
val network = network ?: return val network = network ?: return
for (node in network) { for (node in network) {
@ -74,7 +77,7 @@ class ScenarioState {
network.stop() network.stop()
} }
private fun nodeName(name: String) = "Entity$name" private fun nodeName(name: String) = "$name"
private fun newNode(name: String): Node.Builder { private fun newNode(name: String): Node.Builder {
val builder = Node.new() val builder = Node.new()

View File

@ -1,3 +0,0 @@
package net.corda.behave.scenarios
typealias StepsBlock = (StepsContainer.() -> Unit) -> Unit

View File

@ -1,60 +1,50 @@
package net.corda.behave.scenarios package net.corda.behave.scenarios
import cucumber.api.java8.En import cucumber.api.java8.En
import net.corda.behave.scenarios.helpers.Cash import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
import net.corda.behave.scenarios.helpers.Database import net.corda.behave.scenarios.api.StepsBlock
import net.corda.behave.scenarios.helpers.Ssh import net.corda.behave.scenarios.api.StepsProvider
import net.corda.behave.scenarios.helpers.Startup
import net.corda.behave.scenarios.steps.* import net.corda.behave.scenarios.steps.*
import net.corda.core.messaging.CordaRPCOps import net.corda.core.internal.objectOrNewInstance
import org.slf4j.Logger import net.corda.core.utilities.loggerFor
import org.slf4j.LoggerFactory
@Suppress("KDocMissingDocumentation") @Suppress("KDocMissingDocumentation")
class StepsContainer(val state: ScenarioState) : En { class StepsContainer(val state: ScenarioState) : En {
private val log: Logger = LoggerFactory.getLogger(StepsContainer::class.java) companion object {
val stepsProviders: List<StepsProvider> 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( private val log = loggerFor<StepsContainer>()
::cashSteps,
::configurationSteps, private val stepDefinitions: List<StepsBlock> = listOf(
::databaseSteps, CashSteps(),
::networkSteps, ConfigurationSteps(),
::rpcSteps, DatabaseSteps(),
::sshSteps, NetworkSteps(),
::startupSteps RpcSteps(),
SshSteps(),
StartupSteps(),
VaultSteps()
) )
init { 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 steps(action: (StepsContainer.() -> Unit)) {
fun fail(message: String) = state.fail(message)
fun<T> error(message: String) = state.error<T>(message)
fun node(name: String) = state.nodeBuilder(name)
fun withNetwork(action: ScenarioState.() -> Unit) {
state.withNetwork(action)
}
fun <T> 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)) {
action(this) action(this)
} }
} }

View File

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

View File

@ -0,0 +1,6 @@
package net.corda.behave.scenarios.api
interface StepsProvider {
val name: String
val stepsDefinition: StepsBlock
}

View File

@ -1,8 +1,16 @@
package net.corda.behave.scenarios.helpers package net.corda.behave.scenarios.helpers
import net.corda.behave.scenarios.ScenarioState 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.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.CashConfigDataFlow
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class Cash(state: ScenarioState) : Substeps(state) { 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
}
}
}
} }

View File

@ -1,6 +1,10 @@
package net.corda.behave.scenarios.helpers 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 net.corda.behave.scenarios.ScenarioState
import java.io.File
class Startup(state: ScenarioState) : Substeps(state) { 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()) { if (!node(nodeName).nodeInfoGenerationOutput.find("Logs can be found in.*").any()) {
fail("Unable to find logging information for node $nodeName") 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) { fun hasPlatformVersion(nodeName: String, platformVersion: Int) {
withNetwork { withNetwork {
log.info("Finding platform version for node '$nodeName' ...") log.info("Finding platform version for node '$nodeName' ...")
@ -51,7 +75,7 @@ class Startup(state: ScenarioState) : Substeps(state) {
if (match == null) { if (match == null) {
fail("Unable to find version for node '$nodeName'") fail("Unable to find version for node '$nodeName'")
} else { } else {
val foundVersion = Regex("Version: ([^ ]+) ") val foundVersion = Regex("Release: ([^ ]+) ")
.find(match.contents) .find(match.contents)
?.groups?.last()?.value ?.groups?.last()?.value
fail("Expected version $version for node '$nodeName', " + 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<String>, cordappDirectory, 1.minutes)
command.start()
if (!command.waitFor())
fail("Failed to successfully run the CorDapp jar: $cordaApp")
}
}
} }

View File

@ -20,5 +20,4 @@ abstract class Substeps(protected val state: ScenarioState) {
} }
}) })
} }
} }

View File

@ -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 <T: ContractState> query(nodeName: String, contractStateType: Class<out T>): List<StateAndRef<T>>{
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
}
}
}
}

View File

@ -1,20 +1,37 @@
package net.corda.behave.scenarios.steps 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 import org.assertj.core.api.Assertions.assertThat
fun cashSteps(steps: StepsBlock) = steps { class CashSteps : StepsBlock {
Then<String>("^node (\\w+) has 1 issuable currency$") { name -> override fun initialize(state: ScenarioState) {
withNetwork { val cash = Cash(state)
assertThat(cash.numberOfIssuableCurrencies(name)).isEqualTo(1)
Then<String>("^node (\\w+) has 1 issuable currency$") { name ->
state.withNetwork {
assertThat(cash.numberOfIssuableCurrencies(name)).isEqualTo(1)
}
}
Then<String, String>("^node (\\w+) has (\\w+) issuable currencies$") { name, count ->
state.withNetwork {
assertThat(cash.numberOfIssuableCurrencies(name)).isEqualTo(count.toInt())
}
}
Then<String, Long, String, String>("^node (\\w+) can transfer (\\d+) (\\w+) to node (\\w+)$") { nodeA, amount, currency, nodeB ->
state.withNetwork {
cash.transferCash(nodeA, nodeB, amount, currency)
}
}
Then<String, Long, String>("^node (\\w+) can issue (\\d+) (\\w+)$") { nodeA, amount, currency ->
state.withNetwork {
cash.issueCash(nodeA, amount, currency)
}
} }
} }
Then<String, String>("^node (\\w+) has (\\w+) issuable currencies$") { name, count ->
withNetwork {
assertThat(cash.numberOfIssuableCurrencies(name)).isEqualTo(count.toInt())
}
}
} }

View File

@ -3,47 +3,64 @@ package net.corda.behave.scenarios.steps
import net.corda.behave.database.DatabaseType import net.corda.behave.database.DatabaseType
import net.corda.behave.node.Distribution import net.corda.behave.node.Distribution
import net.corda.behave.node.configuration.toNotaryType 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<String, String>("^a node (\\w+) of version ([^ ]+)$") { name, version ->
node(name)
.withDistribution(Distribution.fromVersionString(version))
}
Given<String, String, String, String>("^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<String, String, String>("^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<String, String, String>("^a (\\w+) notary (\\w+) of version ([^ ]+)$") { type, name, version ->
node(name)
.withDistribution(Distribution.fromVersionString(version))
.withNotaryType(type.toNotaryType()
?: error("Unknown notary type '$type'"))
}
Given<String, String>("^node (\\w+) uses database of type (.+)$") { name, type ->
node(name)
.withDatabaseType(DatabaseType.fromName(type)
?: error("Unknown database type '$type'"))
}
Given<String, String>("^node (\\w+) can issue currencies of denomination (.+)$") { name, currencies ->
node(name).withIssuableCurrencies(currencies
.replace(" and ", ", ")
.split(", ")
.map { it.toUpperCase() })
}
Given<String, String, String>("^node (\\w+) is located in (\\w+), (\\w+)$") { name, location, country ->
node(name).withLocation(location, country)
}
Given<String>("^node (\\w+) has the finance app installed$") { name ->
node(name).withFinanceApp()
}
Given<String, String>("^node (\\w+) has app installed: (.+)$") { name, app ->
node(name).withApp(app)
}
Given<String, String>("^a node (\\w+) of version ([^ ]+)$") { name, version ->
node(name)
.withDistribution(Distribution.fromVersionString(version)
?: error("Unknown version '$version'"))
} }
Given<String, String, String>("^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<String, String>("^node (\\w+) uses database of type (.+)$") { name, type ->
node(name)
.withDatabaseType(DatabaseType.fromName(type)
?: error("Unknown database type '$type'"))
}
Given<String, String>("^node (\\w+) can issue (.+)$") { name, currencies ->
node(name).withIssuableCurrencies(currencies
.replace(" and ", ", ")
.split(", ")
.map { it.toUpperCase() })
}
Given<String, String, String>("^node (\\w+) is located in (\\w+), (\\w+)$") { name, location, country ->
node(name).withLocation(location, country)
}
Given<String>("^node (\\w+) has the finance app installed$") { name ->
node(name).withFinanceApp()
}
Given<String, String>("^node (\\w+) has app installed: (.+)$") { name, app ->
node(name).withApp(app)
}
} }

View File

@ -1,13 +1,18 @@
package net.corda.behave.scenarios.steps 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<String>("^user can connect to the database of node (\\w+)$") { name -> override fun initialize(state: ScenarioState) {
withNetwork { val database = Database(state)
database.canConnectTo(name)
Then<String>("^user can connect to the database of node (\\w+)$") { name ->
state.withNetwork {
database.canConnectTo(name)
}
} }
} }
} }

View File

@ -1,11 +1,18 @@
package net.corda.behave.scenarios.steps 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$") { override fun initialize(state: ScenarioState) {
state.ensureNetworkIsRunning() When("^the network is ready$") {
state.ensureNetworkIsRunning()
}
When<Int>("^the network is ready within (\\d+) minutes$") { minutes ->
state.ensureNetworkIsRunning(minutes.minutes)
}
} }
} }

View File

@ -1,13 +1,15 @@
package net.corda.behave.scenarios.steps 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<String>("^user can connect to node (\\w+) using RPC$") { name -> override fun initialize(state: ScenarioState) {
withClient(name) { Then<String>("^user can connect to node (\\w+) using RPC$") { name ->
succeed() state.withClient(name) {
succeed()
}
} }
} }
} }

View File

@ -1,13 +1,18 @@
package net.corda.behave.scenarios.steps 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<String>("^user can connect to node (\\w+) using SSH$") { name -> override fun initialize(state: ScenarioState) {
withNetwork { val ssh = Ssh(state)
ssh.canConnectTo(name)
Then<String>("^user can connect to node (\\w+) using SSH$") { name ->
state.withNetwork {
ssh.canConnectTo(name)
}
} }
} }
} }

View File

@ -1,31 +1,66 @@
package net.corda.behave.scenarios.steps 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<String>("^user can retrieve database details for node (\\w+)$") { name -> override fun initialize(state: ScenarioState) {
withNetwork { val startup = Startup(state)
startup.hasDatabaseDetails(name)
Then<String>("^user can retrieve database details for node (\\w+)$") { name ->
state.withNetwork {
startup.hasDatabaseDetails(name)
}
}
Then<String>("^user can retrieve logging information for node (\\w+)$") { name ->
state.withNetwork {
startup.hasLoggingInformation(name)
}
}
Then<String, String>("^node (\\w+) is on release version ([^ ]+)$") { name, version ->
state.withNetwork {
startup.hasVersion(name, version)
}
}
Then<String, String>("^node (\\w+) is on platform version (\\w+)$") { name, platformVersion ->
state.withNetwork {
startup.hasPlatformVersion(name, platformVersion.toInt())
}
}
Then<String>("^user can retrieve node identity information for node (\\w+)") { name ->
state.withNetwork {
startup.hasIdentityDetails(name)
}
}
Then<String, String>("^node (\\w+) has loaded app (.+)$") { name, cordapp ->
state.withNetwork {
startup.hasLoadedCordapp(name, cordapp)
}
}
Then<String, String>("^node (\\w+) can run (\\w+)\$") { name, cordapp ->
state.withNetwork {
startup.runCordapp(name, cordapp)
}
}
Then<String, String, String>("^node (\\w+) can run (\\w+) (\\w+)\$") { name, cordapp, arg1 ->
state.withNetwork {
startup.runCordapp(name, cordapp, arg1)
}
}
Then<String, String, String, String>("^node (\\w+) can run (\\w+) (\\w+) (\\w+)\$") { name, cordapp, arg1, arg2 ->
state.withNetwork {
startup.runCordapp(name, cordapp, arg1, arg2)
}
} }
} }
Then<String>("^user can retrieve logging information for node (\\w+)$") { name ->
withNetwork {
startup.hasLoggingInformation(name)
}
}
Then<String, String>("^node (\\w+) is on version ([^ ]+)$") { name, version ->
withNetwork {
startup.hasVersion(name, version)
}
}
Then<String, String>("^node (\\w+) is on platform version (\\w+)$") { name, platformVersion ->
withNetwork {
startup.hasPlatformVersion(name, platformVersion.toInt())
}
}
} }

View File

@ -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<String, Int>("^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<String, Int, String>("^node (\\w+) vault contains (\\d+) (\\w+) states$") { node, count, contractType ->
try {
val contractStateTypeClass = Class.forName(contractType) as Class<ContractState>
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<String, Long, String>("^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")
}
}
}

View File

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

View File

@ -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. 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 Scenario: Node can issue no currencies by default
Given a node A of version master Given a node PartyA of version master
And node A has the finance app installed And node PartyA has the finance app installed
When the network is ready 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 Scenario: Node can issue a currency
Given a node A of version master Given a node PartyA of version master
And node A can issue USD And a nonvalidating notary Notary of version master
And node PartyA has the finance app installed
When the network is ready When the network is ready
Then node A has 1 issuable currency Then node PartyA can issue 100 USD

View File

@ -3,12 +3,12 @@ Feature: Database - Connection
For Corda to work, a database must be running and appropriately configured. For Corda to work, a database must be running and appropriately configured.
Scenario Outline: User can connect to node's database Scenario Outline: User can connect to node's database
Given a node A of version <Node-Version> Given a node PartyA of version <Node-Version>
And node A uses database of type <Database-Type> And node PartyA uses database of type <Database-Type>
When the network is ready 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: Examples:
| Node-Version | Database-Type | | Node-Version | Database-Type |
| MASTER | H2 | | master | H2 |
#| MASTER | SQL Server | # | master | postgreSQL |

View File

@ -4,18 +4,18 @@ Feature: Startup Information - Logging
configure / connect relevant software to said node. configure / connect relevant software to said node.
Scenario: Node shows logging information on startup Scenario: Node shows logging information on startup
Given a node A of version MASTER Given a node PartyA of version master
And node A uses database of type H2 And node PartyA uses database of type H2
And node A is located in London, GB And node PartyA is located in London, GB
When the network is ready 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 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 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 Scenario: Node shows version information on startup
Given a node A of version MASTER Given a node PartyA of version master
Then node A is on platform version 2 Then node PartyA is on platform version 4
And node A is on version 3.0-SNAPSHOT And node PartyA is on release version corda-4.0-snapshot

View File

@ -10,55 +10,55 @@ class MonitoringTests {
@Test @Test
fun `watch gets triggered when pattern is observed`() { fun `watch gets triggered when pattern is observed`() {
val observable = Observable.just("first", "second", "third") 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() assertThat(result).isTrue()
} }
@Test @Test
fun `watch does not get triggered when pattern is not observed`() { fun `watch does not get triggered when pattern is not observed`() {
val observable = Observable.just("first", "second", "third") 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() assertThat(result).isFalse()
} }
@Test @Test
fun `conjunctive watch gets triggered when all its constituents match on the input`() { fun `conjunctive watch gets triggered when all its constituents match on the input`() {
val observable = Observable.just("first", "second", "third") val observable = Observable.just("first", "second", "third")
val watch1 = PatternWatch("fir") val watch1 = PatternWatch(observable, "fir")
val watch2 = PatternWatch("ond") val watch2 = PatternWatch(observable, "ond")
val watch3 = PatternWatch("ird") val watch3 = PatternWatch(observable, "ird")
val aggregate = watch1 * watch2 * watch3 val aggregate = watch1 * watch2 * watch3
assertThat(aggregate.await(observable, 1.second)).isTrue() assertThat(aggregate.await(1.second)).isTrue()
} }
@Test @Test
fun `conjunctive watch does not get triggered when one or more of its constituents do not match on the input`() { 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 observable = Observable.just("first", "second", "third")
val watch1 = PatternWatch("fir") val watch1 = PatternWatch(observable, "fir")
val watch2 = PatternWatch("ond") val watch2 = PatternWatch(observable, "ond")
val watch3 = PatternWatch("baz") val watch3 = PatternWatch(observable, "baz")
val aggregate = watch1 * watch2 * watch3 val aggregate = watch1 * watch2 * watch3
assertThat(aggregate.await(observable, 1.second)).isFalse() assertThat(aggregate.await(1.second)).isFalse()
} }
@Test @Test
fun `disjunctive watch gets triggered when one or more of its constituents match on the input`() { fun `disjunctive watch gets triggered when one or more of its constituents match on the input`() {
val observable = Observable.just("first", "second", "third") val observable = Observable.just("first", "second", "third")
val watch1 = PatternWatch("foo") val watch1 = PatternWatch(observable, "foo")
val watch2 = PatternWatch("ond") val watch2 = PatternWatch(observable, "ond")
val watch3 = PatternWatch("bar") val watch3 = PatternWatch(observable, "bar")
val aggregate = watch1 / watch2 / watch3 val aggregate = watch1 / watch2 / watch3
assertThat(aggregate.await(observable, 1.second)).isTrue() assertThat(aggregate.await(1.second)).isTrue()
} }
@Test @Test
fun `disjunctive watch does not get triggered when none its constituents match on the input`() { fun `disjunctive watch does not get triggered when none its constituents match on the input`() {
val observable = Observable.just("first", "second", "third") val observable = Observable.just("first", "second", "third")
val watch1 = PatternWatch("foo") val watch1 = PatternWatch(observable, "foo")
val watch2 = PatternWatch("baz") val watch2 = PatternWatch(observable, "baz")
val watch3 = PatternWatch("bar") val watch3 = PatternWatch(observable, "bar")
val aggregate = watch1 / watch2 / watch3 val aggregate = watch1 / watch2 / watch3
assertThat(aggregate.await(observable, 1.second)).isFalse() assertThat(aggregate.await(1.second)).isFalse()
} }
} }

View File

@ -29,7 +29,7 @@ class NetworkTests {
val network = Network val network = Network
.new() .new()
.addNode("Foo") .addNode("Foo")
.addNode("Bar", databaseType = DatabaseType.SQL_SERVER) .addNode("Bar", databaseType = DatabaseType.POSTGRES)
.addNode("Baz", notaryType = NotaryType.NON_VALIDATING) .addNode("Baz", notaryType = NotaryType.NON_VALIDATING)
.generate() .generate()
network.use { network.use {

View File

@ -1,6 +1,6 @@
package net.corda.behave.process package net.corda.behave.process
import org.assertj.core.api.Assertions.* import org.assertj.core.api.Assertions.assertThat
import org.junit.Test import org.junit.Test
import rx.observers.TestSubscriber import rx.observers.TestSubscriber
@ -30,5 +30,4 @@ class CommandTests {
} }
assertThat(exitCode).isEqualTo(0) assertThat(exitCode).isEqualTo(0)
} }
} }

View File

@ -1,16 +1,16 @@
package net.corda.behave.service 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.assertj.core.api.Assertions.assertThat
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
class SqlServerServiceTests { class PostreSQLServiceTests {
@Ignore @Ignore
@Test @Test
fun `sql server can be started and stopped`() { fun `postgres can be started and stopped`() {
val service = SqlServerService("test-mssql", 12345, "S0meS3cretW0rd") val service = PostgreSQLService("test-postgres", 12345, "postgres")
val didStart = service.start() val didStart = service.start()
service.stop() service.stop()
assertThat(didStart).isTrue() assertThat(didStart).isTrue()