mirror of
https://github.com/corda/corda.git
synced 2024-12-24 07:06:44 +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) {
|
||||
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
|
||||
dependencies so that you easily can spin up a Corda node locally
|
||||
that, for instance, uses a 3rd party database provider such as
|
||||
MS SQL Server or Postgres.
|
||||
Postgres.
|
||||
|
||||
# Structure
|
||||
|
||||
|
@ -66,6 +66,12 @@ dependencies {
|
||||
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||
compile "org.apache.logging.log4j:log4j-core:$log4j_version"
|
||||
|
||||
// JOptSimple: command line option parsing
|
||||
compile "net.sf.jopt-simple:jopt-simple:$jopt_simple_version"
|
||||
|
||||
// FastClasspathScanner: classpath scanning
|
||||
compile 'io.github.lukehutch:fast-classpath-scanner:2.12.3'
|
||||
|
||||
compile "commons-io:commons-io:$commonsio_version"
|
||||
compile "commons-logging:commons-logging:$commonslogging_version"
|
||||
compile "com.spotify:docker-client:$docker_client_version"
|
||||
@ -85,7 +91,6 @@ dependencies {
|
||||
scenarioCompile "info.cukes:cucumber-java8:$cucumber_version"
|
||||
scenarioCompile "info.cukes:cucumber-junit:$cucumber_version"
|
||||
scenarioCompile "info.cukes:cucumber-picocontainer:$cucumber_version"
|
||||
|
||||
}
|
||||
|
||||
compileKotlin {
|
||||
|
@ -1,3 +1,4 @@
|
||||
Download and store database drivers here; for example:
|
||||
- h2-1.4.196.jar
|
||||
- mssql-jdbc-6.2.2.jre8.jar
|
||||
- postgresql-42.1.4.jar
|
||||
|
@ -1,24 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
VERSION=3.0.0
|
||||
set -x
|
||||
|
||||
# Please ensure you run this script using source code (eg. GitHub master, branch or TAG) that reflects the version label defined below
|
||||
# For example:
|
||||
# corda-master => git clone https://github.com/corda/corda
|
||||
# r3corda-master => git clone https://github.com/corda/enterprise
|
||||
VERSION=corda-3.0
|
||||
STAGING_DIR=deps/corda/${VERSION}
|
||||
DRIVERS_DIR=deps/drivers
|
||||
|
||||
# Set up directories
|
||||
mkdir -p deps/corda/${VERSION}/apps
|
||||
mkdir -p deps/drivers
|
||||
mkdir -p ${STAGING_DIR}/apps
|
||||
mkdir -p ${DRIVERS_DIR}
|
||||
|
||||
# Copy Corda capsule into deps
|
||||
cp -v $(ls ../../node/capsule/build/libs/corda-*.jar | tail -n1) deps/corda/${VERSION}/corda.jar
|
||||
cd ../..
|
||||
./gradlew clean :node:capsule:buildCordaJar :finance:jar
|
||||
cp -v $(ls node/capsule/build/libs/corda-*.jar | tail -n1) experimental/behave/${STAGING_DIR}/corda.jar
|
||||
|
||||
# Copy finance library
|
||||
cp -v $(ls finance/build/libs/corda-finance-*.jar | tail -n1) experimental/behave/${STAGING_DIR}/apps
|
||||
|
||||
# Download database drivers
|
||||
curl "https://search.maven.org/remotecontent?filepath=com/h2database/h2/1.4.196/h2-1.4.196.jar" > deps/drivers/h2-1.4.196.jar
|
||||
curl -L "https://github.com/Microsoft/mssql-jdbc/releases/download/v6.2.2/mssql-jdbc-6.2.2.jre8.jar" > deps/drivers/mssql-jdbc-6.2.2.jre8.jar
|
||||
curl "https://search.maven.org/remotecontent?filepath=com/h2database/h2/1.4.196/h2-1.4.196.jar" > experimental/behave/${DRIVERS_DIR}/h2-1.4.196.jar
|
||||
curl -L "http://central.maven.org/maven2/org/postgresql/postgresql/42.1.4/postgresql-42.1.4.jar" > experimental/behave/${DRIVERS_DIR}/postgresql-42.1.4.jar
|
||||
|
||||
# Build required artefacts
|
||||
cd ../..
|
||||
# Build Network Bootstrapper
|
||||
./gradlew buildBootstrapperJar
|
||||
./gradlew :finance:jar
|
||||
|
||||
# Copy build artefacts into deps
|
||||
cd experimental/behave
|
||||
cp -v $(ls ../../tools/bootstrapper/build/libs/*.jar | tail -n1) deps/corda/${VERSION}/network-bootstrapper.jar
|
||||
cp -v $(ls ../../finance/build/libs/corda-finance-*.jar | tail -n1) deps/corda/${VERSION}/apps/corda-finance.jar
|
||||
cp -v $(ls tools/bootstrapper/build/libs/*.jar | tail -n1) experimental/behave/${STAGING_DIR}/network-bootstrapper.jar
|
||||
|
@ -16,6 +16,9 @@ class DatabaseSettings {
|
||||
var userName: String = "sa"
|
||||
private set
|
||||
|
||||
var driverJar: String? = null
|
||||
private set
|
||||
|
||||
private var databaseConfigTemplate: DatabaseConfigurationTemplate = DatabaseConfigurationTemplate()
|
||||
|
||||
private val serviceInitiators = mutableListOf<ServiceInitiator>()
|
||||
@ -30,6 +33,11 @@ class DatabaseSettings {
|
||||
return this
|
||||
}
|
||||
|
||||
fun withDriver(name: String): DatabaseSettings {
|
||||
driverJar = name
|
||||
return this
|
||||
}
|
||||
|
||||
fun withUser(name: String): DatabaseSettings {
|
||||
userName = name
|
||||
return this
|
||||
|
@ -1,11 +1,11 @@
|
||||
package net.corda.behave.database
|
||||
|
||||
import net.corda.behave.database.configuration.H2ConfigurationTemplate
|
||||
import net.corda.behave.database.configuration.SqlServerConfigurationTemplate
|
||||
import net.corda.behave.database.configuration.PostgresConfigurationTemplate
|
||||
import net.corda.behave.node.configuration.Configuration
|
||||
import net.corda.behave.node.configuration.DatabaseConfiguration
|
||||
import net.corda.behave.service.database.H2Service
|
||||
import net.corda.behave.service.database.SqlServerService
|
||||
import net.corda.behave.service.database.PostgreSQLService
|
||||
|
||||
enum class DatabaseType(val settings: DatabaseSettings) {
|
||||
|
||||
@ -19,16 +19,19 @@ enum class DatabaseType(val settings: DatabaseSettings) {
|
||||
}
|
||||
),
|
||||
|
||||
SQL_SERVER(DatabaseSettings()
|
||||
.withDatabase(SqlServerService.database)
|
||||
.withSchema(SqlServerService.schema)
|
||||
.withUser(SqlServerService.username)
|
||||
.withConfigTemplate(SqlServerConfigurationTemplate())
|
||||
POSTGRES(DatabaseSettings()
|
||||
.withDatabase(PostgreSQLService.database)
|
||||
.withDriver(PostgreSQLService.driver)
|
||||
.withSchema(PostgreSQLService.schema)
|
||||
.withUser(PostgreSQLService.username)
|
||||
.withConfigTemplate(PostgresConfigurationTemplate())
|
||||
.withServiceInitiator {
|
||||
SqlServerService("sqlserver-${it.name}", it.database.port, it.database.password)
|
||||
PostgreSQLService("postgres-${it.name}", it.database.port, it.database.password)
|
||||
}
|
||||
);
|
||||
|
||||
val driverJar = settings.driverJar
|
||||
|
||||
fun dependencies(config: Configuration) = settings.dependencies(config)
|
||||
|
||||
fun connection(config: DatabaseConfiguration) = DatabaseConnection(config, settings.template)
|
||||
@ -37,9 +40,8 @@ enum class DatabaseType(val settings: DatabaseSettings) {
|
||||
|
||||
fun fromName(name: String): DatabaseType? = when (name.toLowerCase()) {
|
||||
"h2" -> H2
|
||||
"sql_server" -> SQL_SERVER
|
||||
"sql server" -> SQL_SERVER
|
||||
"sqlserver" -> SQL_SERVER
|
||||
"postgres" -> POSTGRES
|
||||
"postgresql" -> POSTGRES
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
@ -3,26 +3,23 @@ package net.corda.behave.database.configuration
|
||||
import net.corda.behave.database.DatabaseConfigurationTemplate
|
||||
import net.corda.behave.node.configuration.DatabaseConfiguration
|
||||
|
||||
class SqlServerConfigurationTemplate : DatabaseConfigurationTemplate() {
|
||||
class PostgresConfigurationTemplate : DatabaseConfigurationTemplate() {
|
||||
|
||||
override val connectionString: (DatabaseConfiguration) -> String
|
||||
get() = { "jdbc:sqlserver://${it.host}:${it.port};database=${it.database}" }
|
||||
get() = { "jdbc:postgresql://${it.host}:${it.port}/${it.database}" }
|
||||
|
||||
override val config: (DatabaseConfiguration) -> String
|
||||
get() = {
|
||||
"""
|
||||
|dataSourceProperties = {
|
||||
| dataSourceClassName = "com.microsoft.sqlserver.jdbc.SQLServerDataSource"
|
||||
| dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource"
|
||||
| dataSource.url = "${connectionString(it)}"
|
||||
| dataSource.user = "${it.username}"
|
||||
| dataSource.password = "${it.password}"
|
||||
|}
|
||||
|database = {
|
||||
| initialiseSchema=true
|
||||
| transactionIsolationLevel = READ_COMMITTED
|
||||
| schema="${it.schema}"
|
||||
|}
|
||||
"""
|
||||
}
|
||||
|
||||
}
|
@ -5,4 +5,10 @@ import java.io.File
|
||||
val currentDirectory: File
|
||||
get() = File(System.getProperty("user.dir"))
|
||||
|
||||
// location of Corda distributions and Drivers dependencies
|
||||
val stagingRoot: File
|
||||
get() = if (System.getProperty("STAGING_ROOT") != null)
|
||||
File(System.getProperty("STAGING_ROOT"))
|
||||
else currentDirectory
|
||||
|
||||
operator fun File.div(relative: String): File = this.resolve(relative)
|
||||
|
@ -1,23 +1,24 @@
|
||||
package net.corda.behave.monitoring
|
||||
|
||||
import net.corda.behave.await
|
||||
import rx.Observable
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
class ConjunctiveWatch(
|
||||
private val left: Watch,
|
||||
private val right: Watch
|
||||
) : Watch() {
|
||||
) : Watch {
|
||||
|
||||
override fun await(observable: Observable<String>, timeout: Duration): Boolean {
|
||||
val latch = CountDownLatch(2)
|
||||
override fun ready() = left.ready() && right.ready()
|
||||
|
||||
override fun await(timeout: Duration): Boolean {
|
||||
val countDownLatch = CountDownLatch(2)
|
||||
listOf(left, right).parallelStream().forEach {
|
||||
if (it.await(observable, timeout)) {
|
||||
latch.countDown()
|
||||
if (it.await(timeout)) {
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
return latch.await(timeout)
|
||||
return countDownLatch.await(timeout)
|
||||
}
|
||||
|
||||
}
|
@ -8,16 +8,18 @@ import java.util.concurrent.CountDownLatch
|
||||
class DisjunctiveWatch(
|
||||
private val left: Watch,
|
||||
private val right: Watch
|
||||
) : Watch() {
|
||||
) : Watch {
|
||||
|
||||
override fun await(observable: Observable<String>, timeout: Duration): Boolean {
|
||||
val latch = CountDownLatch(1)
|
||||
override fun ready() = left.ready() || right.ready()
|
||||
|
||||
override fun await(timeout: Duration): Boolean {
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
listOf(left, right).parallelStream().forEach {
|
||||
if (it.await(observable, timeout)) {
|
||||
latch.countDown()
|
||||
if (it.await(timeout)) {
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
return latch.await(timeout)
|
||||
return countDownLatch.await(timeout)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,22 +1,24 @@
|
||||
package net.corda.behave.monitoring
|
||||
|
||||
import rx.Observable
|
||||
|
||||
class PatternWatch(
|
||||
observable: Observable<String>,
|
||||
pattern: String,
|
||||
ignoreCase: Boolean = false
|
||||
) : Watch() {
|
||||
) : AbstractWatch<String>(observable, false) {
|
||||
|
||||
private val regularExpression = if (ignoreCase) {
|
||||
private val regularExpression: Regex = if (ignoreCase) {
|
||||
Regex("^.*$pattern.*$", RegexOption.IGNORE_CASE)
|
||||
} else {
|
||||
Regex("^.*$pattern.*$")
|
||||
}
|
||||
|
||||
override fun match(data: String) = regularExpression.matches(data.trim())
|
||||
|
||||
companion object {
|
||||
|
||||
val EMPTY = PatternWatch("")
|
||||
|
||||
init {
|
||||
run()
|
||||
}
|
||||
|
||||
override fun match(data: String): Boolean {
|
||||
return regularExpression.matches(data.trim())
|
||||
}
|
||||
}
|
@ -6,28 +6,46 @@ import rx.Observable
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
abstract class Watch {
|
||||
|
||||
private val latch = CountDownLatch(1)
|
||||
|
||||
open fun await(
|
||||
observable: Observable<String>,
|
||||
timeout: Duration = 10.seconds
|
||||
): Boolean {
|
||||
observable
|
||||
.filter { match(it) }
|
||||
.forEach { latch.countDown() }
|
||||
return latch.await(timeout)
|
||||
}
|
||||
|
||||
open fun match(data: String): Boolean = false
|
||||
interface Watch {
|
||||
fun await(timeout: Duration = 10.seconds): Boolean
|
||||
fun ready(): Boolean
|
||||
|
||||
operator fun times(other: Watch): Watch {
|
||||
return ConjunctiveWatch(this, other)
|
||||
}
|
||||
|
||||
operator fun div(other: Watch): Watch {
|
||||
return DisjunctiveWatch(this, other)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [observable] refers to an observable stream of events
|
||||
* @param [autostart] is true starting of Watch can be deferred - it helps in case of initialization
|
||||
* order problems (like match()) using fields from subclass which won't get initialized before superclass
|
||||
* constructor finishes. It is the responsibility of the subclass to manually call the run method
|
||||
* if autostart is false.
|
||||
*/
|
||||
abstract class AbstractWatch<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.currentDirectory
|
||||
import net.corda.behave.file.div
|
||||
import net.corda.behave.file.stagingRoot
|
||||
import net.corda.behave.logging.getLogger
|
||||
import net.corda.behave.minutes
|
||||
import net.corda.behave.node.Distribution
|
||||
import net.corda.behave.node.Node
|
||||
import net.corda.behave.node.configuration.NotaryType
|
||||
import net.corda.behave.process.JarCommand
|
||||
import net.corda.core.CordaException
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
@ -26,8 +28,6 @@ class Network private constructor(
|
||||
private val timeout: Duration = 2.minutes
|
||||
) : Closeable, Iterable<Node> {
|
||||
|
||||
private val log = getLogger<Network>()
|
||||
|
||||
private val latch = CountDownLatch(1)
|
||||
|
||||
private var isRunning = false
|
||||
@ -36,6 +36,10 @@ class Network private constructor(
|
||||
|
||||
private var hasError = false
|
||||
|
||||
init {
|
||||
FileUtils.forceMkdir(targetDirectory)
|
||||
}
|
||||
|
||||
class Builder internal constructor(
|
||||
private val timeout: Duration
|
||||
) {
|
||||
@ -43,7 +47,7 @@ class Network private constructor(
|
||||
private val nodes = mutableMapOf<String, Node>()
|
||||
|
||||
private val startTime = DateTimeFormatter
|
||||
.ofPattern("yyyyMMDD-HHmmss")
|
||||
.ofPattern("yyyyMMdd-HHmmss")
|
||||
.withZone(ZoneId.of("UTC"))
|
||||
.format(Instant.now())
|
||||
|
||||
@ -51,7 +55,7 @@ class Network private constructor(
|
||||
|
||||
fun addNode(
|
||||
name: String,
|
||||
distribution: Distribution = Distribution.LATEST_MASTER,
|
||||
distribution: Distribution = Distribution.MASTER,
|
||||
databaseType: DatabaseType = DatabaseType.H2,
|
||||
notaryType: NotaryType = NotaryType.NONE,
|
||||
issuableCurrencies: List<String> = emptyList()
|
||||
@ -76,22 +80,28 @@ class Network private constructor(
|
||||
|
||||
fun generate(): Network {
|
||||
val network = Network(nodes, directory, timeout)
|
||||
network.bootstrapNetwork()
|
||||
|
||||
network.copyDatabaseDrivers()
|
||||
if (!network.configureNodes()) {
|
||||
throw CordaException("Unable to configure nodes in Corda network. Please check logs in $directory")
|
||||
}
|
||||
network.bootstrapLocalNetwork()
|
||||
|
||||
return network
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun copyDatabaseDrivers() {
|
||||
fun copyDatabaseDrivers() {
|
||||
val driverDirectory = targetDirectory / "libs"
|
||||
log.info("Copying database drivers from $stagingRoot/deps/drivers to $driverDirectory")
|
||||
FileUtils.forceMkdir(driverDirectory)
|
||||
FileUtils.copyDirectory(
|
||||
currentDirectory / "deps/drivers",
|
||||
stagingRoot / "deps/drivers",
|
||||
driverDirectory
|
||||
)
|
||||
}
|
||||
|
||||
private fun configureNodes(): Boolean {
|
||||
fun configureNodes(): Boolean {
|
||||
var allDependenciesStarted = true
|
||||
log.info("Configuring nodes ...")
|
||||
for (node in nodes.values) {
|
||||
@ -109,19 +119,14 @@ class Network private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun bootstrapNetwork() {
|
||||
copyDatabaseDrivers()
|
||||
if (!configureNodes()) {
|
||||
hasError = true
|
||||
return
|
||||
}
|
||||
private fun bootstrapLocalNetwork() {
|
||||
val bootstrapper = nodes.values
|
||||
.sortedByDescending { it.config.distribution.version }
|
||||
.first()
|
||||
.config.distribution.networkBootstrapper
|
||||
|
||||
if (!bootstrapper.exists()) {
|
||||
log.warn("Network bootstrapping tool does not exist; continuing ...")
|
||||
signalFailure("Network bootstrapping tool does not exist; exiting ...")
|
||||
return
|
||||
}
|
||||
|
||||
@ -205,11 +210,15 @@ class Network private constructor(
|
||||
}
|
||||
isRunning = true
|
||||
for (node in nodes.values) {
|
||||
log.info("Starting node [{}]", node.config.name)
|
||||
node.start()
|
||||
}
|
||||
}
|
||||
|
||||
fun waitUntilRunning(waitDuration: Duration? = null): Boolean {
|
||||
|
||||
log.info("Network.waitUntilRunning")
|
||||
|
||||
if (hasError) {
|
||||
return false
|
||||
}
|
||||
@ -264,6 +273,7 @@ class Network private constructor(
|
||||
}
|
||||
log.info("Shutting down network ...")
|
||||
isStopped = true
|
||||
log.info("Shutting down nodes ...")
|
||||
for (node in nodes.values) {
|
||||
node.shutDown()
|
||||
}
|
||||
@ -289,13 +299,10 @@ class Network private constructor(
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val log = getLogger<Network>()
|
||||
const val CLEANUP_ON_ERROR = false
|
||||
|
||||
fun new(
|
||||
timeout: Duration = 2.minutes
|
||||
fun new(timeout: Duration = 2.minutes
|
||||
): Builder = Builder(timeout)
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
package net.corda.behave.node
|
||||
|
||||
import net.corda.behave.file.div
|
||||
import net.corda.behave.file.stagingRoot
|
||||
import net.corda.behave.logging.getLogger
|
||||
import net.corda.behave.service.Service
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
@ -23,39 +26,53 @@ class Distribution private constructor(
|
||||
/**
|
||||
* The URL of the distribution fat JAR, if available.
|
||||
*/
|
||||
val url: URL? = null
|
||||
val url: URL? = null,
|
||||
|
||||
/**
|
||||
* The Docker image details, if available
|
||||
*/
|
||||
val baseImage: String? = null
|
||||
) {
|
||||
|
||||
/**
|
||||
* The path to the distribution fat JAR.
|
||||
*/
|
||||
val jarFile: File = file ?: nodePrefix / "$version/corda.jar"
|
||||
val path: File = file ?: nodePrefix / "$version"
|
||||
|
||||
/**
|
||||
* The path to the distribution fat JAR.
|
||||
*/
|
||||
val cordaJar: File = path / "corda.jar"
|
||||
|
||||
/**
|
||||
* The path to available Cordapps for this distribution.
|
||||
*/
|
||||
val cordappDirectory: File = nodePrefix / "$version/apps"
|
||||
val cordappDirectory: File = path / "apps"
|
||||
|
||||
/**
|
||||
* The path to network bootstrapping tool.
|
||||
*/
|
||||
val networkBootstrapper: File = nodePrefix / "$version/network-bootstrapper.jar"
|
||||
val networkBootstrapper: File = path / "network-bootstrapper.jar"
|
||||
|
||||
/**
|
||||
* Ensure that the distribution is available on disk.
|
||||
*/
|
||||
fun ensureAvailable() {
|
||||
if (!jarFile.exists()) {
|
||||
if (!cordaJar.exists()) {
|
||||
if (url != null) {
|
||||
try {
|
||||
FileUtils.forceMkdirParent(jarFile)
|
||||
FileUtils.copyURLToFile(url, jarFile)
|
||||
FileUtils.forceMkdirParent(cordaJar)
|
||||
FileUtils.copyURLToFile(url, cordaJar)
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Invalid Corda version $version", e)
|
||||
if (e.message!!.contains("HTTP response code: 401")) {
|
||||
log.warn("CORDA_ARTIFACTORY_USERNAME ${System.getenv("CORDA_ARTIFACTORY_USERNAME")}")
|
||||
log.warn("CORDA_ARTIFACTORY_PASSWORD ${System.getenv("CORDA_ARTIFACTORY_PASSWORD")}")
|
||||
throw Exception("Incorrect Artifactory permission. Please set CORDA_ARTIFACTORY_USERNAME and CORDA_ARTIFACTORY_PASSWORD environment variables correctly.")
|
||||
}
|
||||
else throw Exception("Invalid Corda version $version", e)
|
||||
}
|
||||
} else {
|
||||
throw Exception("File not found $jarFile")
|
||||
throw Exception("File not found $cordaJar")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -63,29 +80,25 @@ class Distribution private constructor(
|
||||
/**
|
||||
* Human-readable representation of the distribution.
|
||||
*/
|
||||
override fun toString() = "Corda(version = $version, path = $jarFile)"
|
||||
override fun toString() = "Corda(version = $version, path = $cordaJar)"
|
||||
|
||||
companion object {
|
||||
|
||||
protected val log = getLogger<Service>()
|
||||
|
||||
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")
|
||||
|
||||
val LATEST_MASTER = V3
|
||||
|
||||
/**
|
||||
* Get representation of an open source distribution based on its version string.
|
||||
* @param version The version of the open source Corda distribution.
|
||||
*/
|
||||
fun fromOpenSourceVersion(version: String): Distribution {
|
||||
val url = URL("https://dl.bintray.com/r3/corda/net/corda/corda/$version/corda-$version.jar")
|
||||
fun fromArtifactory(version: String): Distribution {
|
||||
val url = URL("https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases/net/corda/corda/$version/corda-$version.jar")
|
||||
log.info("Artifactory URL: $url\n")
|
||||
val distribution = Distribution(version, url = url)
|
||||
distributions.add(distribution)
|
||||
return distribution
|
||||
@ -102,15 +115,25 @@ class Distribution private constructor(
|
||||
return distribution
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Corda distribution from a Docker image file.
|
||||
* @param baseImage The name (eg. corda) of the Corda distribution.
|
||||
* @param imageTag The version (github commit id or corda version) of the Corda distribution.
|
||||
*/
|
||||
fun fromDockerImage(baseImage: String, imageTag: String): Distribution {
|
||||
val distribution = Distribution(version = imageTag, baseImage = baseImage)
|
||||
distributions.add(distribution)
|
||||
return distribution
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registered representation of a Corda distribution based on its version string.
|
||||
* @param version The version of the Corda distribution
|
||||
*/
|
||||
fun fromVersionString(version: String): Distribution? = when (version.toLowerCase()) {
|
||||
"master" -> LATEST_MASTER
|
||||
else -> distributions.firstOrNull { it.version == version }
|
||||
fun fromVersionString(version: String): Distribution = when (version) {
|
||||
"master" -> MASTER
|
||||
"corda-3.0" -> fromArtifactory(version)
|
||||
else -> fromJarFile(version)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import net.corda.behave.database.DatabaseType
|
||||
import net.corda.behave.file.LogSource
|
||||
import net.corda.behave.file.currentDirectory
|
||||
import net.corda.behave.file.div
|
||||
import net.corda.behave.file.stagingRoot
|
||||
import net.corda.behave.logging.getLogger
|
||||
import net.corda.behave.monitoring.PatternWatch
|
||||
import net.corda.behave.node.configuration.*
|
||||
@ -20,6 +21,7 @@ import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
import java.net.InetAddress
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@ -39,14 +41,14 @@ class Node(
|
||||
private val logDirectory = runtimeDirectory / "logs"
|
||||
|
||||
private val command = JarCommand(
|
||||
config.distribution.jarFile,
|
||||
config.distribution.cordaJar,
|
||||
arrayOf("--config", "node.conf"),
|
||||
runtimeDirectory,
|
||||
settings.timeout,
|
||||
enableRemoteDebugging = false
|
||||
)
|
||||
|
||||
private val isAliveLatch = PatternWatch("Node for \".*\" started up and registered")
|
||||
private val isAliveLatch = PatternWatch(command.output, "Node for \".*\" started up and registered")
|
||||
|
||||
private var isConfigured = false
|
||||
|
||||
@ -76,7 +78,7 @@ class Node(
|
||||
log.info("Configuring {} ...", this)
|
||||
serviceDependencies.addAll(config.database.type.dependencies(config))
|
||||
config.distribution.ensureAvailable()
|
||||
config.writeToFile(rootDirectory / "${config.name}.conf")
|
||||
config.writeToFile(rootDirectory / "${config.name}_node.conf")
|
||||
installApps()
|
||||
}
|
||||
|
||||
@ -97,7 +99,7 @@ class Node(
|
||||
}
|
||||
|
||||
fun waitUntilRunning(waitDuration: Duration? = null): Boolean {
|
||||
val ok = isAliveLatch.await(command.output, waitDuration ?: settings.timeout)
|
||||
val ok = isAliveLatch.await(waitDuration ?: settings.timeout)
|
||||
if (!ok) {
|
||||
log.warn("{} did not start up as expected within the given time frame", this)
|
||||
} else {
|
||||
@ -126,7 +128,8 @@ class Node(
|
||||
}
|
||||
|
||||
val logOutput: LogSource by lazy {
|
||||
LogSource(logDirectory, "node-info-gen.log", filePatternUsedForExclusion = true)
|
||||
val hostname = InetAddress.getLocalHost().hostName
|
||||
LogSource(logDirectory, "node-$hostname.*.log")
|
||||
}
|
||||
|
||||
val database: DatabaseConnection by lazy {
|
||||
@ -216,7 +219,7 @@ class Node(
|
||||
|
||||
private fun installApps() {
|
||||
val version = config.distribution.version
|
||||
val appDirectory = rootDirectory / "../../../deps/corda/$version/apps"
|
||||
val appDirectory = stagingRoot / "deps/corda/$version/apps"
|
||||
if (appDirectory.exists()) {
|
||||
val targetAppDirectory = runtimeDirectory / "cordapps"
|
||||
FileUtils.copyDirectory(appDirectory, targetAppDirectory)
|
||||
@ -228,7 +231,7 @@ class Node(
|
||||
var name: String? = null
|
||||
private set
|
||||
|
||||
private var distribution = Distribution.V3
|
||||
private var distribution = Distribution.MASTER
|
||||
|
||||
private var databaseType = DatabaseType.H2
|
||||
|
||||
@ -314,13 +317,14 @@ class Node(
|
||||
databaseType,
|
||||
location = location,
|
||||
country = country,
|
||||
notary = NotaryConfiguration(notaryType),
|
||||
cordapps = CordappConfiguration(
|
||||
apps = apps,
|
||||
includeFinance = includeFinance
|
||||
),
|
||||
configElements = *arrayOf(
|
||||
NotaryConfiguration(notaryType),
|
||||
CurrencyConfiguration(issuableCurrencies),
|
||||
CordappConfiguration(
|
||||
apps = *apps.toTypedArray(),
|
||||
includeFinance = includeFinance
|
||||
)
|
||||
CurrencyConfiguration(issuableCurrencies)
|
||||
)
|
||||
),
|
||||
directory,
|
||||
@ -331,7 +335,6 @@ class Node(
|
||||
private fun <T> error(message: String): T {
|
||||
throw IllegalArgumentException(message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -1,13 +1,15 @@
|
||||
package net.corda.behave.node.configuration
|
||||
|
||||
import net.corda.behave.database.DatabaseType
|
||||
import net.corda.behave.logging.getLogger
|
||||
import net.corda.behave.node.*
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
|
||||
class Configuration(
|
||||
val name: String,
|
||||
val distribution: Distribution = Distribution.LATEST_MASTER,
|
||||
val distribution: Distribution = Distribution.MASTER,
|
||||
val databaseType: DatabaseType = DatabaseType.H2,
|
||||
val location: String = "London",
|
||||
val country: String = "GB",
|
||||
@ -19,19 +21,21 @@ class Configuration(
|
||||
nodeInterface.dbPort,
|
||||
password = DEFAULT_PASSWORD
|
||||
),
|
||||
val notary: NotaryConfiguration = NotaryConfiguration(),
|
||||
val cordapps: CordappConfiguration = CordappConfiguration(),
|
||||
vararg configElements: ConfigurationTemplate
|
||||
) {
|
||||
|
||||
private val developerMode = true
|
||||
|
||||
private val useHttps = false
|
||||
val cordaX500Name: CordaX500Name by lazy({
|
||||
CordaX500Name(name, location, country)
|
||||
})
|
||||
|
||||
private val basicConfig = """
|
||||
|myLegalName="C=$country,L=$location,O=$name"
|
||||
|keyStorePassword="cordacadevpass"
|
||||
|trustStorePassword="trustpass"
|
||||
|extraAdvertisedServiceIds=[ "" ]
|
||||
|useHTTPS=$useHttps
|
||||
|devMode=$developerMode
|
||||
|jarDirs = [ "../libs" ]
|
||||
""".trimMargin()
|
||||
@ -41,6 +45,7 @@ class Configuration(
|
||||
|
||||
fun writeToFile(file: File) {
|
||||
FileUtils.writeStringToFile(file, this.generate(), "UTF-8")
|
||||
log.info(this.generate())
|
||||
}
|
||||
|
||||
private fun generate() = listOf(basicConfig, database.config(), extraConfig)
|
||||
@ -48,9 +53,8 @@ class Configuration(
|
||||
.joinToString("\n")
|
||||
|
||||
companion object {
|
||||
|
||||
private val DEFAULT_PASSWORD = "S0meS3cretW0rd"
|
||||
|
||||
private val log = getLogger<Configuration>()
|
||||
val DEFAULT_PASSWORD = "S0meS3cretW0rd"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package net.corda.behave.node.configuration
|
||||
|
||||
class CordappConfiguration(vararg apps: String, var includeFinance: Boolean = false) : ConfigurationTemplate() {
|
||||
class CordappConfiguration(var apps: List<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")
|
||||
} else {
|
||||
emptyList()
|
||||
|
@ -8,9 +8,11 @@ class CurrencyConfiguration(private val issuableCurrencies: List<String>) : Conf
|
||||
""
|
||||
} else {
|
||||
"""
|
||||
|issuableCurrencies=[
|
||||
| ${issuableCurrencies.joinToString(", ")}
|
||||
|]
|
||||
|custom : {
|
||||
| issuableCurrencies : [
|
||||
| ${issuableCurrencies.joinToString(", ")}
|
||||
| ]
|
||||
|}
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,8 @@ data class NetworkInterface(
|
||||
val rpcPort: Int = getPort(12002 + (nodeIndex * 5)),
|
||||
val rpcAdminPort: Int = getPort(12003 + (nodeIndex * 5)),
|
||||
val webPort: Int = getPort(12004 + (nodeIndex * 5)),
|
||||
val dbPort: Int = getPort(12005 + (nodeIndex * 5))
|
||||
val dbPort: Int = getPort(12005 + (nodeIndex * 5)),
|
||||
val dockerPort: Int = getPort(5000 + (nodeIndex * 5))
|
||||
) : ConfigurationTemplate() {
|
||||
|
||||
init {
|
||||
@ -28,7 +29,6 @@ data class NetworkInterface(
|
||||
| address = "$host:$rpcPort"
|
||||
| adminAddress = "$host:$rpcAdminPort"
|
||||
|}
|
||||
|webAddress="$host:$webPort"
|
||||
"""
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.corda.behave.node.configuration
|
||||
|
||||
class NotaryConfiguration(private val notaryType: NotaryType) : ConfigurationTemplate() {
|
||||
class NotaryConfiguration(private val notaryType: NotaryType = NotaryType.NONE) : ConfigurationTemplate() {
|
||||
|
||||
override val config: (Configuration) -> String
|
||||
get() = {
|
||||
|
@ -27,12 +27,12 @@ open class Command(
|
||||
|
||||
private var process: Process? = null
|
||||
|
||||
private lateinit var outputListener: OutputListener
|
||||
private var outputListener: OutputListener? = null
|
||||
|
||||
var exitCode = -1
|
||||
private set
|
||||
|
||||
val output: Observable<String> = Observable.create<String> { emitter ->
|
||||
val output: Observable<String> = Observable.create<String>({ emitter ->
|
||||
outputListener = object : OutputListener {
|
||||
override fun onNewLine(line: String) {
|
||||
emitter.onNext(line)
|
||||
@ -42,10 +42,11 @@ open class Command(
|
||||
emitter.onCompleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
}).share()
|
||||
|
||||
private val thread = Thread(Runnable {
|
||||
try {
|
||||
log.info("Command: $command")
|
||||
val processBuilder = ProcessBuilder(command)
|
||||
.directory(directory)
|
||||
.redirectErrorStream(true)
|
||||
@ -57,13 +58,17 @@ open class Command(
|
||||
while (true) {
|
||||
try {
|
||||
val line = input.readLine()?.trimEnd() ?: break
|
||||
outputListener.onNewLine(line)
|
||||
log.trace(line)
|
||||
outputListener?.onNewLine(line)
|
||||
} catch (_: IOException) {
|
||||
break
|
||||
} catch (ex: Exception) {
|
||||
log.error("Unexpected exception during reading input", ex)
|
||||
break
|
||||
}
|
||||
}
|
||||
input.close()
|
||||
outputListener.onEndOfStream()
|
||||
outputListener?.onEndOfStream()
|
||||
outputCapturedLatch.countDown()
|
||||
}).start()
|
||||
val streamIsClosed = outputCapturedLatch.await(timeout)
|
||||
@ -88,13 +93,15 @@ open class Command(
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.warn("Error occurred when trying to run process", e)
|
||||
throw e
|
||||
}
|
||||
finally {
|
||||
process = null
|
||||
terminationLatch.countDown()
|
||||
}
|
||||
process = null
|
||||
terminationLatch.countDown()
|
||||
})
|
||||
|
||||
fun start() {
|
||||
output.subscribe()
|
||||
thread.start()
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import java.io.File
|
||||
import java.time.Duration
|
||||
|
||||
class JarCommand(
|
||||
jarFile: File,
|
||||
val jarFile: File,
|
||||
arguments: Array<String>,
|
||||
directory: File,
|
||||
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.PortBinding
|
||||
import net.corda.behave.monitoring.PatternWatch
|
||||
import net.corda.behave.monitoring.Watch
|
||||
import rx.Observable
|
||||
import java.io.Closeable
|
||||
|
||||
abstract class ContainerService(
|
||||
name: String,
|
||||
port: Int,
|
||||
val startupStatement: String,
|
||||
settings: ServiceSettings = ServiceSettings()
|
||||
) : Service(name, port, settings), Closeable {
|
||||
|
||||
@ -30,8 +30,6 @@ abstract class ContainerService(
|
||||
|
||||
private val environmentVariables: MutableList<String> = mutableListOf()
|
||||
|
||||
private var startupStatement: Watch = PatternWatch.EMPTY
|
||||
|
||||
private val imageReference: String
|
||||
get() = "$baseImage:$imageTag"
|
||||
|
||||
@ -51,7 +49,12 @@ abstract class ContainerService(
|
||||
|
||||
val creation = client.createContainer(containerConfig)
|
||||
id = creation.id()
|
||||
|
||||
val info = client.inspectContainer(id)
|
||||
log.info("Container $id info: $info")
|
||||
|
||||
client.startContainer(id)
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
id = null
|
||||
@ -73,10 +76,6 @@ abstract class ContainerService(
|
||||
environmentVariables.add("$name=$value")
|
||||
}
|
||||
|
||||
protected fun setStartupStatement(statement: String) {
|
||||
startupStatement = PatternWatch(statement)
|
||||
}
|
||||
|
||||
override fun checkPrerequisites() {
|
||||
if (!client.listImages().any { true == it.repoTags()?.contains(imageReference) }) {
|
||||
log.info("Pulling image $imageReference ...")
|
||||
@ -97,8 +96,8 @@ abstract class ContainerService(
|
||||
while (timeout > 0) {
|
||||
client.logs(id, DockerClient.LogsParam.stdout(), DockerClient.LogsParam.stderr()).use {
|
||||
val contents = it.readFully()
|
||||
val observable = Observable.from(contents.split("\n"))
|
||||
if (startupStatement.await(observable, settings.pollInterval)) {
|
||||
val observable = Observable.from(contents.split("\n", "\r"))
|
||||
if (PatternWatch(observable, startupStatement).await(settings.pollInterval)) {
|
||||
log.info("Found process start-up statement for {}", this)
|
||||
return true
|
||||
}
|
||||
@ -118,5 +117,4 @@ abstract class ContainerService(
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,31 +2,25 @@ package net.corda.behave.service.database
|
||||
|
||||
import net.corda.behave.database.DatabaseConnection
|
||||
import net.corda.behave.database.DatabaseType
|
||||
import net.corda.behave.database.configuration.SqlServerConfigurationTemplate
|
||||
import net.corda.behave.database.configuration.PostgresConfigurationTemplate
|
||||
import net.corda.behave.node.configuration.DatabaseConfiguration
|
||||
import net.corda.behave.service.ContainerService
|
||||
import net.corda.behave.service.ServiceSettings
|
||||
|
||||
class SqlServerService(
|
||||
class PostgreSQLService(
|
||||
name: String,
|
||||
port: Int,
|
||||
private val password: String,
|
||||
settings: ServiceSettings = ServiceSettings()
|
||||
) : ContainerService(name, port, settings) {
|
||||
) : ContainerService(name, port, "database system is ready to accept connections", settings) {
|
||||
|
||||
override val baseImage = "microsoft/mssql-server-linux"
|
||||
override val baseImage = "postgres"
|
||||
|
||||
override val internalPort = 1433
|
||||
|
||||
init {
|
||||
addEnvironmentVariable("ACCEPT_EULA", "Y")
|
||||
addEnvironmentVariable("SA_PASSWORD", password)
|
||||
setStartupStatement("SQL Server is now ready for client connections")
|
||||
}
|
||||
override val internalPort = 5432
|
||||
|
||||
override fun verify(): Boolean {
|
||||
val config = DatabaseConfiguration(
|
||||
type = DatabaseType.SQL_SERVER,
|
||||
type = DatabaseType.POSTGRES,
|
||||
host = host,
|
||||
port = port,
|
||||
database = database,
|
||||
@ -34,7 +28,7 @@ class SqlServerService(
|
||||
username = username,
|
||||
password = password
|
||||
)
|
||||
val connection = DatabaseConnection(config, SqlServerConfigurationTemplate())
|
||||
val connection = DatabaseConnection(config, PostgresConfigurationTemplate())
|
||||
try {
|
||||
connection.use {
|
||||
return true
|
||||
@ -47,12 +41,11 @@ class SqlServerService(
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val host = "localhost"
|
||||
val database = "master"
|
||||
val schema = "dbo"
|
||||
val username = "sa"
|
||||
|
||||
val database = "postgres"
|
||||
val schema = "public"
|
||||
val username = "postgres"
|
||||
val driver = "postgresql-42.1.4.jar"
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
import cucumber.api.java.After
|
||||
import net.corda.behave.logging.getLogger
|
||||
import net.corda.behave.network.Network
|
||||
import net.corda.behave.node.Node
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import java.time.Duration
|
||||
|
||||
class ScenarioState {
|
||||
|
||||
@ -36,7 +38,7 @@ class ScenarioState {
|
||||
return nodes.firstOrNull { it.name == nodeName(name) } ?: newNode(name)
|
||||
}
|
||||
|
||||
fun ensureNetworkIsRunning() {
|
||||
fun ensureNetworkIsRunning(timeout: Duration? = null) {
|
||||
if (network != null) {
|
||||
// Network is already running
|
||||
return
|
||||
@ -47,7 +49,7 @@ class ScenarioState {
|
||||
}
|
||||
network = networkBuilder.generate()
|
||||
network?.start()
|
||||
assertThat(network?.waitUntilRunning()).isTrue()
|
||||
assertThat(network?.waitUntilRunning(timeout)).isTrue()
|
||||
}
|
||||
|
||||
inline fun <T> withNetwork(action: ScenarioState.() -> T): T {
|
||||
@ -63,6 +65,7 @@ class ScenarioState {
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun stopNetwork() {
|
||||
val network = network ?: return
|
||||
for (node in network) {
|
||||
@ -74,7 +77,7 @@ class ScenarioState {
|
||||
network.stop()
|
||||
}
|
||||
|
||||
private fun nodeName(name: String) = "Entity$name"
|
||||
private fun nodeName(name: String) = "$name"
|
||||
|
||||
private fun newNode(name: String): Node.Builder {
|
||||
val builder = Node.new()
|
||||
|
@ -1,3 +0,0 @@
|
||||
package net.corda.behave.scenarios
|
||||
|
||||
typealias StepsBlock = (StepsContainer.() -> Unit) -> Unit
|
@ -1,60 +1,50 @@
|
||||
package net.corda.behave.scenarios
|
||||
|
||||
import cucumber.api.java8.En
|
||||
import net.corda.behave.scenarios.helpers.Cash
|
||||
import net.corda.behave.scenarios.helpers.Database
|
||||
import net.corda.behave.scenarios.helpers.Ssh
|
||||
import net.corda.behave.scenarios.helpers.Startup
|
||||
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
|
||||
import net.corda.behave.scenarios.api.StepsBlock
|
||||
import net.corda.behave.scenarios.api.StepsProvider
|
||||
import net.corda.behave.scenarios.steps.*
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import net.corda.core.internal.objectOrNewInstance
|
||||
import net.corda.core.utilities.loggerFor
|
||||
|
||||
@Suppress("KDocMissingDocumentation")
|
||||
class StepsContainer(val state: ScenarioState) : En {
|
||||
|
||||
private val log: Logger = LoggerFactory.getLogger(StepsContainer::class.java)
|
||||
companion object {
|
||||
val stepsProviders: List<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(
|
||||
::cashSteps,
|
||||
::configurationSteps,
|
||||
::databaseSteps,
|
||||
::networkSteps,
|
||||
::rpcSteps,
|
||||
::sshSteps,
|
||||
::startupSteps
|
||||
private val log = loggerFor<StepsContainer>()
|
||||
|
||||
private val stepDefinitions: List<StepsBlock> = listOf(
|
||||
CashSteps(),
|
||||
ConfigurationSteps(),
|
||||
DatabaseSteps(),
|
||||
NetworkSteps(),
|
||||
RpcSteps(),
|
||||
SshSteps(),
|
||||
StartupSteps(),
|
||||
VaultSteps()
|
||||
)
|
||||
|
||||
init {
|
||||
stepDefinitions.forEach { it({ this.steps(it) }) }
|
||||
log.info("Initialising common Steps Provider ...")
|
||||
stepDefinitions.forEach { it.initialize(state) }
|
||||
log.info("Searching and registering custom Steps Providers ...")
|
||||
stepsProviders.forEach { stepsProvider ->
|
||||
val stepsDefinition = stepsProvider.stepsDefinition
|
||||
log.info("Registering: $stepsDefinition")
|
||||
stepsDefinition.initialize(state)
|
||||
}
|
||||
}
|
||||
|
||||
fun succeed() = log.info("Step succeeded")
|
||||
|
||||
fun fail(message: String) = state.fail(message)
|
||||
|
||||
fun<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)) {
|
||||
fun steps(action: (StepsContainer.() -> Unit)) {
|
||||
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
|
||||
|
||||
import net.corda.behave.scenarios.ScenarioState
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.flows.CashConfigDataFlow
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class Cash(state: ScenarioState) : Substeps(state) {
|
||||
@ -28,4 +36,34 @@ class Cash(state: ScenarioState) : Substeps(state) {
|
||||
}
|
||||
}
|
||||
|
||||
fun issueCash(issueToNode: String, amount: Long, currency: String): SignedTransaction {
|
||||
return withClient(issueToNode) {
|
||||
try {
|
||||
val notaryList = it.notaryIdentities()
|
||||
if (notaryList.isEmpty())
|
||||
throw CordaRuntimeException("No Notaries configured in this network.")
|
||||
val notaryParty = notaryList[0]
|
||||
return@withClient it.startFlow(::CashIssueFlow, Amount(amount, Currency.getInstance(currency)), OpaqueBytes.of(1), notaryParty).returnValue.getOrThrow().stx
|
||||
} catch (ex: Exception) {
|
||||
log.warn("Failed to issue $amount $currency cash to $issueToNode", ex)
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun transferCash(senderNode: String, sendToNode: String, amount: Long, currency: String): SignedTransaction {
|
||||
return withClient(senderNode) {
|
||||
try {
|
||||
val sendToX500Name = node(sendToNode).config.cordaX500Name
|
||||
val sendToParty = node(senderNode).rpc {
|
||||
it.wellKnownPartyFromX500Name(sendToX500Name) ?: throw IllegalStateException("Unable to locate $sendToX500Name in Network Map Service")
|
||||
}
|
||||
return@withClient it.startFlow(::CashPaymentFlow, Amount(amount, Currency.getInstance(currency)), sendToParty).returnValue.getOrThrow().stx
|
||||
} catch (ex: Exception) {
|
||||
log.warn("Failed to transfer $amount cash from $senderNode to $sendToNode", ex)
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,10 @@
|
||||
package net.corda.behave.scenarios.helpers
|
||||
|
||||
import net.corda.behave.file.div
|
||||
import net.corda.behave.minutes
|
||||
import net.corda.behave.process.JarCommand
|
||||
import net.corda.behave.scenarios.ScenarioState
|
||||
import java.io.File
|
||||
|
||||
class Startup(state: ScenarioState) : Substeps(state) {
|
||||
|
||||
@ -10,6 +14,13 @@ class Startup(state: ScenarioState) : Substeps(state) {
|
||||
if (!node(nodeName).nodeInfoGenerationOutput.find("Logs can be found in.*").any()) {
|
||||
fail("Unable to find logging information for node $nodeName")
|
||||
}
|
||||
|
||||
withClient(nodeName) {
|
||||
log.info("$nodeName: ${it.nodeInfo()} has registered flows:")
|
||||
for (flow in it.registeredFlows()) {
|
||||
log.info(flow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +33,19 @@ class Startup(state: ScenarioState) : Substeps(state) {
|
||||
}
|
||||
}
|
||||
|
||||
fun hasIdentityDetails(nodeName: String) {
|
||||
withNetwork {
|
||||
log.info("Retrieving identity details for node '$nodeName' ...")
|
||||
try {
|
||||
val nodeInfo = node(nodeName).rpc { it.nodeInfo() }
|
||||
log.info("\nNode $nodeName identity details: $nodeInfo\n")
|
||||
} catch (ex: Exception) {
|
||||
log.warn("Failed to retrieve node identity details", ex)
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun hasPlatformVersion(nodeName: String, platformVersion: Int) {
|
||||
withNetwork {
|
||||
log.info("Finding platform version for node '$nodeName' ...")
|
||||
@ -51,7 +75,7 @@ class Startup(state: ScenarioState) : Substeps(state) {
|
||||
if (match == null) {
|
||||
fail("Unable to find version for node '$nodeName'")
|
||||
} else {
|
||||
val foundVersion = Regex("Version: ([^ ]+) ")
|
||||
val foundVersion = Regex("Release: ([^ ]+) ")
|
||||
.find(match.contents)
|
||||
?.groups?.last()?.value
|
||||
fail("Expected version $version for node '$nodeName', " +
|
||||
@ -62,4 +86,29 @@ class Startup(state: ScenarioState) : Substeps(state) {
|
||||
}
|
||||
}
|
||||
|
||||
fun hasLoadedCordapp(nodeName: String, cordappName: String) {
|
||||
withNetwork {
|
||||
log.info("Checking CorDapp $cordappName is loaded in node $nodeName ...\n")
|
||||
val logOutput = node(nodeName).logOutput
|
||||
if (!logOutput.find(".*Loaded CorDapps.*$cordappName.*").any()) {
|
||||
fail("Unable to find $cordappName loaded in node $nodeName")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun runCordapp(nodeName: String, cordapp: String, vararg args: String) {
|
||||
withNetwork {
|
||||
val cordaApp = node(nodeName).config.cordapps.apps.find { it.contains(cordapp) } ?: fail("Unable to locate CorDapp: $cordapp")
|
||||
// launch cordapp jar
|
||||
// assumption is there is a Main() method declared in the manifest of the JAR
|
||||
// eg. Main-Class: net.corda.notaryhealthcheck.MainKt
|
||||
val cordappDirectory = node(nodeName).config.distribution.cordappDirectory
|
||||
val cordappJar : File = cordappDirectory / "$cordapp.jar"
|
||||
// Execute
|
||||
val command = JarCommand(cordappJar, args as Array<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
|
||||
|
||||
import net.corda.behave.scenarios.StepsBlock
|
||||
import net.corda.behave.scenarios.ScenarioState
|
||||
import net.corda.behave.scenarios.api.StepsBlock
|
||||
import net.corda.behave.scenarios.helpers.Cash
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
|
||||
fun cashSteps(steps: StepsBlock) = steps {
|
||||
class CashSteps : StepsBlock {
|
||||
|
||||
Then<String>("^node (\\w+) has 1 issuable currency$") { name ->
|
||||
withNetwork {
|
||||
assertThat(cash.numberOfIssuableCurrencies(name)).isEqualTo(1)
|
||||
override fun initialize(state: ScenarioState) {
|
||||
val cash = Cash(state)
|
||||
|
||||
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.node.Distribution
|
||||
import net.corda.behave.node.configuration.toNotaryType
|
||||
import net.corda.behave.scenarios.StepsBlock
|
||||
import net.corda.behave.scenarios.ScenarioState
|
||||
import net.corda.behave.scenarios.api.StepsBlock
|
||||
|
||||
fun configurationSteps(steps: StepsBlock) = steps {
|
||||
class ConfigurationSteps : StepsBlock {
|
||||
|
||||
override fun initialize(state: ScenarioState) {
|
||||
fun node(name: String) = state.nodeBuilder(name)
|
||||
|
||||
Given<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
|
||||
|
||||
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 ->
|
||||
withNetwork {
|
||||
database.canConnectTo(name)
|
||||
override fun initialize(state: ScenarioState) {
|
||||
val database = Database(state)
|
||||
|
||||
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
|
||||
|
||||
import net.corda.behave.scenarios.StepsBlock
|
||||
import net.corda.behave.scenarios.ScenarioState
|
||||
import net.corda.behave.scenarios.api.StepsBlock
|
||||
import net.corda.core.utilities.minutes
|
||||
|
||||
fun networkSteps(steps: StepsBlock) = steps {
|
||||
class NetworkSteps : StepsBlock {
|
||||
|
||||
When("^the network is ready$") {
|
||||
state.ensureNetworkIsRunning()
|
||||
override fun initialize(state: ScenarioState) {
|
||||
When("^the network is ready$") {
|
||||
state.ensureNetworkIsRunning()
|
||||
}
|
||||
|
||||
When<Int>("^the network is ready within (\\d+) minutes$") { minutes ->
|
||||
state.ensureNetworkIsRunning(minutes.minutes)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
package net.corda.behave.scenarios.steps
|
||||
|
||||
import net.corda.behave.scenarios.StepsBlock
|
||||
import net.corda.behave.scenarios.ScenarioState
|
||||
import net.corda.behave.scenarios.api.StepsBlock
|
||||
|
||||
fun rpcSteps(steps: StepsBlock) = steps {
|
||||
class RpcSteps : StepsBlock {
|
||||
|
||||
Then<String>("^user can connect to node (\\w+) using RPC$") { name ->
|
||||
withClient(name) {
|
||||
succeed()
|
||||
override fun initialize(state: ScenarioState) {
|
||||
Then<String>("^user can connect to node (\\w+) using RPC$") { name ->
|
||||
state.withClient(name) {
|
||||
succeed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,13 +1,18 @@
|
||||
package net.corda.behave.scenarios.steps
|
||||
|
||||
import net.corda.behave.scenarios.StepsBlock
|
||||
import net.corda.behave.scenarios.ScenarioState
|
||||
import net.corda.behave.scenarios.api.StepsBlock
|
||||
import net.corda.behave.scenarios.helpers.Ssh
|
||||
|
||||
fun sshSteps(steps: StepsBlock) = steps {
|
||||
class SshSteps : StepsBlock {
|
||||
|
||||
Then<String>("^user can connect to node (\\w+) using SSH$") { name ->
|
||||
withNetwork {
|
||||
ssh.canConnectTo(name)
|
||||
override fun initialize(state: ScenarioState) {
|
||||
val ssh = Ssh(state)
|
||||
|
||||
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
|
||||
|
||||
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 ->
|
||||
withNetwork {
|
||||
startup.hasDatabaseDetails(name)
|
||||
override fun initialize(state: ScenarioState) {
|
||||
val startup = Startup(state)
|
||||
|
||||
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.
|
||||
|
||||
Scenario: Node can issue no currencies by default
|
||||
Given a node A of version master
|
||||
And node A has the finance app installed
|
||||
Given a node PartyA of version master
|
||||
And node PartyA has the finance app installed
|
||||
When the network is ready
|
||||
Then node A has 0 issuable currencies
|
||||
Then node PartyA has 0 issuable currencies
|
||||
|
||||
Scenario: Node has an issuable currency
|
||||
Given a node PartyA of version master
|
||||
And node PartyA can issue currencies of denomination USD
|
||||
And node PartyA has the finance app installed
|
||||
When the network is ready
|
||||
Then node PartyA has 1 issuable currency
|
||||
|
||||
Scenario: Node can issue a currency
|
||||
Given a node A of version master
|
||||
And node A can issue USD
|
||||
Given a node PartyA of version master
|
||||
And a nonvalidating notary Notary of version master
|
||||
And node PartyA has the finance app installed
|
||||
When the network is ready
|
||||
Then node A has 1 issuable currency
|
||||
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.
|
||||
|
||||
Scenario Outline: User can connect to node's database
|
||||
Given a node A of version <Node-Version>
|
||||
And node A uses database of type <Database-Type>
|
||||
Given a node PartyA of version <Node-Version>
|
||||
And node PartyA uses database of type <Database-Type>
|
||||
When the network is ready
|
||||
Then user can connect to the database of node A
|
||||
Then user can connect to the database of node PartyA
|
||||
|
||||
Examples:
|
||||
| Node-Version | Database-Type |
|
||||
| MASTER | H2 |
|
||||
#| MASTER | SQL Server |
|
||||
| master | H2 |
|
||||
# | master | postgreSQL |
|
@ -4,18 +4,18 @@ Feature: Startup Information - Logging
|
||||
configure / connect relevant software to said node.
|
||||
|
||||
Scenario: Node shows logging information on startup
|
||||
Given a node A of version MASTER
|
||||
And node A uses database of type H2
|
||||
And node A is located in London, GB
|
||||
Given a node PartyA of version master
|
||||
And node PartyA uses database of type H2
|
||||
And node PartyA is located in London, GB
|
||||
When the network is ready
|
||||
Then user can retrieve logging information for node A
|
||||
Then user can retrieve logging information for node PartyA
|
||||
|
||||
Scenario: Node shows database details on startup
|
||||
Given a node A of version MASTER
|
||||
Given a node PartyA of version master
|
||||
When the network is ready
|
||||
Then user can retrieve database details for node A
|
||||
Then user can retrieve database details for node PartyA
|
||||
|
||||
Scenario: Node shows version information on startup
|
||||
Given a node A of version MASTER
|
||||
Then node A is on platform version 2
|
||||
And node A is on version 3.0-SNAPSHOT
|
||||
Given a node PartyA of version master
|
||||
Then node PartyA is on platform version 4
|
||||
And node PartyA is on release version corda-4.0-snapshot
|
||||
|
@ -10,55 +10,55 @@ class MonitoringTests {
|
||||
@Test
|
||||
fun `watch gets triggered when pattern is observed`() {
|
||||
val observable = Observable.just("first", "second", "third")
|
||||
val result = PatternWatch("c.n").await(observable, 1.second)
|
||||
val result = PatternWatch(observable, "c.n").await(1.second)
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `watch does not get triggered when pattern is not observed`() {
|
||||
val observable = Observable.just("first", "second", "third")
|
||||
val result = PatternWatch("forth").await(observable, 1.second)
|
||||
val result = PatternWatch(observable, "forth").await(1.second)
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `conjunctive watch gets triggered when all its constituents match on the input`() {
|
||||
val observable = Observable.just("first", "second", "third")
|
||||
val watch1 = PatternWatch("fir")
|
||||
val watch2 = PatternWatch("ond")
|
||||
val watch3 = PatternWatch("ird")
|
||||
val watch1 = PatternWatch(observable, "fir")
|
||||
val watch2 = PatternWatch(observable, "ond")
|
||||
val watch3 = PatternWatch(observable, "ird")
|
||||
val aggregate = watch1 * watch2 * watch3
|
||||
assertThat(aggregate.await(observable, 1.second)).isTrue()
|
||||
assertThat(aggregate.await(1.second)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `conjunctive watch does not get triggered when one or more of its constituents do not match on the input`() {
|
||||
val observable = Observable.just("first", "second", "third")
|
||||
val watch1 = PatternWatch("fir")
|
||||
val watch2 = PatternWatch("ond")
|
||||
val watch3 = PatternWatch("baz")
|
||||
val watch1 = PatternWatch(observable, "fir")
|
||||
val watch2 = PatternWatch(observable, "ond")
|
||||
val watch3 = PatternWatch(observable, "baz")
|
||||
val aggregate = watch1 * watch2 * watch3
|
||||
assertThat(aggregate.await(observable, 1.second)).isFalse()
|
||||
assertThat(aggregate.await(1.second)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disjunctive watch gets triggered when one or more of its constituents match on the input`() {
|
||||
val observable = Observable.just("first", "second", "third")
|
||||
val watch1 = PatternWatch("foo")
|
||||
val watch2 = PatternWatch("ond")
|
||||
val watch3 = PatternWatch("bar")
|
||||
val watch1 = PatternWatch(observable, "foo")
|
||||
val watch2 = PatternWatch(observable, "ond")
|
||||
val watch3 = PatternWatch(observable, "bar")
|
||||
val aggregate = watch1 / watch2 / watch3
|
||||
assertThat(aggregate.await(observable, 1.second)).isTrue()
|
||||
assertThat(aggregate.await(1.second)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disjunctive watch does not get triggered when none its constituents match on the input`() {
|
||||
val observable = Observable.just("first", "second", "third")
|
||||
val watch1 = PatternWatch("foo")
|
||||
val watch2 = PatternWatch("baz")
|
||||
val watch3 = PatternWatch("bar")
|
||||
val watch1 = PatternWatch(observable, "foo")
|
||||
val watch2 = PatternWatch(observable, "baz")
|
||||
val watch3 = PatternWatch(observable, "bar")
|
||||
val aggregate = watch1 / watch2 / watch3
|
||||
assertThat(aggregate.await(observable, 1.second)).isFalse()
|
||||
assertThat(aggregate.await(1.second)).isFalse()
|
||||
}
|
||||
|
||||
}
|
@ -29,7 +29,7 @@ class NetworkTests {
|
||||
val network = Network
|
||||
.new()
|
||||
.addNode("Foo")
|
||||
.addNode("Bar", databaseType = DatabaseType.SQL_SERVER)
|
||||
.addNode("Bar", databaseType = DatabaseType.POSTGRES)
|
||||
.addNode("Baz", notaryType = NotaryType.NON_VALIDATING)
|
||||
.generate()
|
||||
network.use {
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.corda.behave.process
|
||||
|
||||
import org.assertj.core.api.Assertions.*
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import rx.observers.TestSubscriber
|
||||
|
||||
@ -30,5 +30,4 @@ class CommandTests {
|
||||
}
|
||||
assertThat(exitCode).isEqualTo(0)
|
||||
}
|
||||
|
||||
}
|
@ -1,16 +1,16 @@
|
||||
package net.corda.behave.service
|
||||
|
||||
import net.corda.behave.service.database.SqlServerService
|
||||
import net.corda.behave.service.database.PostgreSQLService
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
class SqlServerServiceTests {
|
||||
class PostreSQLServiceTests {
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
fun `sql server can be started and stopped`() {
|
||||
val service = SqlServerService("test-mssql", 12345, "S0meS3cretW0rd")
|
||||
fun `postgres can be started and stopped`() {
|
||||
val service = PostgreSQLService("test-postgres", 12345, "postgres")
|
||||
val didStart = service.start()
|
||||
service.stop()
|
||||
assertThat(didStart).isTrue()
|
Loading…
Reference in New Issue
Block a user