Corda Behave extensions for CTS (#2968)

* Updated to Corda 3.0, added support for PostgreSQL, added STAGING_ROOT environment variable, incorporating Watch improvements (Maks), Steps Provider interface for auto-scanning of 3rd party steps providers, re-implemented StepBlocks, new ScenarioRunner executable tool, additional Steps definitions (Vault, issue/transfer cash, cordapps), other minor bug fixes and logging improvements.

* Updates incorporating PR review feedback.

* Reverted back to original - will re-test in ENT.

* Removed all SQL Server code (to be included in ENT only)

* Minor updates following second PR review.

* Fixed broken scenario tests.

* Final fix for PostgreSQL scenario test.
This commit is contained in:
josecoll 2018-04-19 09:56:16 +01:00 committed by GitHub
parent a684507553
commit ec70478d70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 820 additions and 398 deletions

View File

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

View File

@ -5,7 +5,7 @@ and test homogeneous and heterogeneous Corda networks on a local
machine. The framework has built-in support for Dockerised node
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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() = {

View File

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

View File

@ -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,

View File

@ -6,13 +6,13 @@ import com.spotify.docker.client.messages.ContainerConfig
import com.spotify.docker.client.messages.HostConfig
import com.spotify.docker.client.messages.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()
}
}
}

View File

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

View File

@ -1,18 +0,0 @@
package net.corda.behave.scenarios
import cucumber.api.java.After
import cucumber.api.java.Before
@Suppress("KDocMissingDocumentation")
class ScenarioHooks(private val state: ScenarioState) {
@Before
fun beforeScenario() {
}
@After
fun afterScenario() {
state.stopNetwork()
}
}

View File

@ -0,0 +1,59 @@
@file:JvmName("ScenarioRunner")
package net.corda.behave.scenarios
import joptsimple.OptionParser
import kotlin.system.exitProcess
fun main(args: Array<out String>) {
val parser = OptionParser()
val featurePath = parser.accepts("path").withRequiredArg().required().ofType(String::class.java)
.describedAs("Path location of .feature specifications")
val glue = parser.accepts("glue").withOptionalArg().ofType(String::class.java)
.describedAs("location of additional step definitions, hooks and plugins")
.defaultsTo("net.corda.behave.scenarios")
val plugin = parser.accepts("plugin").withOptionalArg().ofType(String::class.java)
.describedAs("register additional plugins (see https://cucumber.io/docs/reference/jvm)")
.defaultsTo("pretty")
val tags = parser.accepts("tags").withOptionalArg().ofType(String::class.java)
.describedAs("only run scenarios marked as @<tag-name>")
val dryRun = parser.accepts("d")
val options = try {
parser.parse(*args)
} catch (e: Exception) {
println(e.message)
printHelp(parser)
exitProcess(1)
}
val cliArgs = listOf("--glue",
options.valueOf(glue),
"--plugin",
options.valueOf(plugin),
options.valueOf(featurePath)) +
(if (options.hasArgument("tags"))
listOf("--tags", options.valueOf(tags))
else emptyList()) +
if (options.has(dryRun)) listOf("-d") else emptyList()
println("Cucumber CLI scenario runner args: $cliArgs")
cucumber.api.cli.Main.main(cliArgs.toTypedArray())
}
private fun printHelp(parser: OptionParser) {
println("""
Usage: ScenarioRunner [options] --path <location of feature scenario definitions>
Examples:
ScenarioRunner -path <features-dir>
ScenarioRunner -path <features-dir>/<name>.feature
ScenarioRunner -path <features-dir>/<name>.feature:3:9
ScenarioRunner -path <features-dir> --plugin html --tags @qa
ScenarioRunner -path <features-dir> --plugin html --tags @compatibility
Please refer to the Cucumber documentation https://cucumber.io/docs/reference/jvm for more info.
""".trimIndent())
parser.printHelpOn(System.out)
}

View File

@ -1,10 +1,12 @@
package net.corda.behave.scenarios
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()

View File

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

View File

@ -1,60 +1,50 @@
package net.corda.behave.scenarios
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)
}
}

View File

@ -0,0 +1,16 @@
package net.corda.behave.scenarios.api
import cucumber.api.java8.En
import net.corda.behave.scenarios.ScenarioState
import net.corda.core.utilities.contextLogger
interface StepsBlock : En {
companion object {
val log = contextLogger()
}
fun initialize(state: ScenarioState)
fun succeed() = log.info("Step succeeded")
}

View File

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

View File

@ -1,8 +1,16 @@
package net.corda.behave.scenarios.helpers
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
}
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,21 @@
package net.corda.behave.scenarios.helpers
import net.corda.behave.scenarios.ScenarioState
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef
class Vault(state: ScenarioState) : Substeps(state) {
fun <T: ContractState> query(nodeName: String, contractStateType: Class<out T>): List<StateAndRef<T>>{
return withClient(nodeName) {
try {
val results = it.vaultQuery(contractStateType)
log.info("Vault query return results: $results")
return@withClient results.states
} catch (ex: Exception) {
log.warn("Failed to retrieve cash configuration data", ex)
throw ex
}
}
}
}

View File

@ -1,20 +1,37 @@
package net.corda.behave.scenarios.steps
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())
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,44 @@
package net.corda.behave.scenarios.steps
import net.corda.behave.scenarios.ScenarioState
import net.corda.behave.scenarios.api.StepsBlock
import net.corda.behave.scenarios.helpers.Vault
import net.corda.core.contracts.ContractState
import net.corda.core.utilities.sumByLong
import net.corda.finance.contracts.asset.Cash
class VaultSteps : StepsBlock {
override fun initialize(state: ScenarioState) {
val vault = Vault(state)
Then<String, Int>("^node (\\w+) vault contains (\\d+) states$") { node, count ->
if (vault.query(node, ContractState::class.java).size == count)
succeed()
else
state.fail("Vault on node $node does not contain expected number of states: $count")
}
Then<String, Int, String>("^node (\\w+) vault contains (\\d+) (\\w+) states$") { node, count, contractType ->
try {
val contractStateTypeClass = Class.forName(contractType) as Class<ContractState>
if (vault.query(node, contractStateTypeClass).size == count)
succeed()
else
state.fail("Vault on node $node does not contain expected number of states: $count")
} catch (e: Exception) {
state.fail("Invalid contract state class type: ${e.message}")
}
}
Then<String, Long, String>("^node (\\w+) vault contains total cash of (\\d+) (\\w+)$") { node, total, currency ->
val cashStates = vault.query(node, Cash.State::class.java)
val sumCashStates = cashStates.filter { it.state.data.amount.token.product.currencyCode == currency }?.sumByLong { it.state.data.amount.quantity }
print((sumCashStates))
if (sumCashStates == total)
succeed()
else
state.fail("Vault on node $node does not contain total cash of : $total")
}
}
}

View File

@ -0,0 +1,32 @@
package net.corda.behave.scenarios.tests
import net.corda.behave.scenarios.ScenarioState
import net.corda.behave.scenarios.StepsContainer
import net.corda.behave.scenarios.api.StepsBlock
import net.corda.behave.scenarios.api.StepsProvider
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class StepsProviderTests {
@Test
fun `module can discover steps providers`() {
val foundProviders = StepsContainer.Companion.stepsProviders
assertThat(foundProviders).hasOnlyElementsOfType(FooStepsProvider::class.java).hasSize(1)
}
class FooStepsProvider : StepsProvider {
override val name: String
get() = "Foo"
override val stepsDefinition: StepsBlock
get() = DummyStepsBlock()
}
class DummyStepsBlock : StepsBlock {
override fun initialize(state: ScenarioState) {
}
}
}

View File

@ -3,13 +3,21 @@ Feature: Cash - Issuable Currencies
To have cash on ledger, certain nodes must have the ability to issue cash of various currencies.
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

View File

@ -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 |

View File

@ -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

View File

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

View File

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

View File

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

View File

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