mirror of
https://github.com/corda/corda.git
synced 2024-12-20 05:28:21 +00:00
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:
parent
a684507553
commit
ec70478d70
@ -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()
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
|
@ -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
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}"
|
|
||||||
|}
|
|}
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -8,9 +8,11 @@ class CurrencyConfiguration(private val issuableCurrencies: List<String>) : Conf
|
|||||||
""
|
""
|
||||||
} else {
|
} else {
|
||||||
"""
|
"""
|
||||||
|issuableCurrencies=[
|
|custom : {
|
||||||
| ${issuableCurrencies.joinToString(", ")}
|
| issuableCurrencies : [
|
||||||
|]
|
| ${issuableCurrencies.joinToString(", ")}
|
||||||
|
| ]
|
||||||
|
|}
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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() = {
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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)
|
||||||
|
}
|
@ -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()
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
package net.corda.behave.scenarios
|
|
||||||
|
|
||||||
typealias StepsBlock = (StepsContainer.() -> Unit) -> Unit
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package net.corda.behave.scenarios.api
|
||||||
|
|
||||||
|
interface StepsProvider {
|
||||||
|
val name: String
|
||||||
|
val stepsDefinition: StepsBlock
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -20,5 +20,4 @@ abstract class Substeps(protected val state: ScenarioState) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
@ -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 |
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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()
|
Loading…
Reference in New Issue
Block a user