Removed experimental/behave (#5212)

The behave framework is in its own repo. This code is no longer used.
This commit is contained in:
Shams Asari 2019-06-11 16:27:48 +01:00 committed by GitHub
parent d72ec57c1c
commit c32e3a9e26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 0 additions and 3500 deletions

View File

@ -1,58 +0,0 @@
# Introduction
This project illustrates how one can use Cucumber / BDD to drive
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
Postgres.
# Structure
The project is split into three pieces:
* **Testing Library** (main) - This library contains auxiliary
functions that help in configuring and bootstrapping Corda
networks on a local machine. The purpose of the library is to
aid in black-box testing and automation.
* **Unit Tests** (test) - These are various tests for the
library described above. Note that there's only limited
coverage for now.
* **BDD Framework** (scenario) - This module shows how to use
BDD-style frameworks to control the testing of Corda networks;
more specifically, using [Cucumber](cucumber.io).
# Setup
To get started, please follow the instructions below:
* Go up to the root directory and build the capsule JAR.
```bash
$ cd ../../
$ ./gradlew install
```
* Come back to this folder and run:
```bash
$ cd experimental/behave
$ ./prepare.sh
```
This script will download necessary database drivers and set up
the dependencies directory with copies of the Corda fat-JAR and
the network bootstrapping tool.
# Selective Runs
If you only want to run tests of a specific tag, you can append
the following parameter to the Gradle command:
```bash
$ ../../gradlew scenario -Ptags="@cash"
# or
$ ../../gradlew scenario -Ptags="@cash,@logging"
```

View File

@ -1,104 +0,0 @@
ext {
commonsio_version = '2.6'
cucumber_version = '1.2.5'
crash_version = 'cce5a00f114343c1145c1d7756e1dd6df3ea984e'
docker_client_version = '8.11.0'
}
group 'net.corda.behave'
apply plugin: 'kotlin'
sourceSets {
behave {
java {
compileClasspath += main.output
runtimeClasspath += main.output
srcDirs = ["src/main/kotlin", "src/scenario/kotlin"]
}
resources.srcDir file('src/scenario/resources')
}
}
configurations {
behaveCompile.extendsFrom testCompile
behaveRuntime.extendsFrom testRuntime
}
dependencies {
// Library
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
compile("com.github.corda.crash:crash.shell:$crash_version") {
exclude group: "org.slf4j", module: "slf4j-jdk14"
exclude group: "org.bouncycastle"
}
compile("com.github.corda.crash:crash.connectors.ssh:$crash_version") {
exclude group: "org.slf4j", module: "slf4j-jdk14"
exclude group: "org.bouncycastle"
}
compile "org.slf4j:log4j-over-slf4j:$slf4j_version"
compile "org.slf4j:jul-to-slf4j:$slf4j_version"
compile "org.slf4j:jcl-over-slf4j:$slf4j_version"
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"
// ClassGraph: classpath scanning
compile "io.github.classgraph:classgraph:$class_graph_version"
compile "commons-io:commons-io:$commonsio_version"
compile "com.spotify:docker-client:$docker_client_version"
compile "io.reactivex:rxjava:$rxjava_version"
compile project(':finance:contracts')
compile project(':finance:workflows')
compile project(':node-api')
compile project(':client:rpc')
// Unit Tests
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
testImplementation "junit:junit:$junit_version"
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
testCompile "org.assertj:assertj-core:$assertj_version"
// Scenarios / End-to-End Tests
behaveCompile "info.cukes:cucumber-java8:$cucumber_version"
behaveCompile "info.cukes:cucumber-junit:$cucumber_version"
behaveCompile "info.cukes:cucumber-picocontainer:$cucumber_version"
}
test {
testLogging.showStandardStreams = true
}
task behaveJar(type: Jar) {
baseName "corda-behave"
from sourceSets.behave.output
from {
configurations.behaveCompile.collect {
it.isDirectory() ? it : zipTree(it)
}
}
zip64 true
exclude("features/**")
exclude("scripts/**")
exclude("META-INF/*.DSA")
exclude("META-INF/*.RSA")
exclude("META-INF/*.SF")
manifest {
attributes 'Main-Class': 'net.corda.behave.scenarios.ScenarioRunner'
}
}

View File

@ -1 +0,0 @@
*.jar

View File

@ -1,4 +0,0 @@
Download and store database drivers here; for example:
- h2-1.4.196.jar
- postgresql-42.1.4.jar

View File

@ -1,35 +0,0 @@
#!/bin/bash
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-master
STAGING_DIR=~/staging
CORDA_DIR=${STAGING_DIR}/corda/${VERSION}
CORDAPP_DIR=${CORDA_DIR}/apps
DRIVERS_DIR=${STAGING_DIR}/drivers
# Set up directories
mkdir -p ${STAGING_DIR}
mkdir -p ${CORDA_DIR}
mkdir -p ${CORDAPP_DIR}
mkdir -p ${DRIVERS_DIR}
# Copy Corda capsule into deps
cd ../..
./gradlew clean :node:capsule:buildCordaJar :finance:jar
cp -v $(ls node/capsule/build/libs/corda-*.jar | tail -n1) ${CORDA_DIR}/corda.jar
# Copy finance library
cp -v $(ls finance/build/libs/corda-finance-*.jar | tail -n1) ${CORDAPP_DIR}
# Download database drivers
curl "https://search.maven.org/remotecontent?filepath=com/h2database/h2/1.4.196/h2-1.4.196.jar" > ${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" > ${DRIVERS_DIR}/postgresql-42.1.4.jar
# Build Network Bootstrapper
./gradlew tools:bootstrapper:jar
cp -v $(ls tools/bootstrapper/build/libs/*.jar | tail -n1) ${CORDA_DIR}/network-bootstrapper.jar

View File

@ -1,10 +0,0 @@
package net.corda.behave
import java.time.Duration
import java.util.concurrent.CountDownLatch
fun CountDownLatch.await(duration: Duration) =
this.await(duration.toMillis(), java.util.concurrent.TimeUnit.MILLISECONDS)
fun Process.waitFor(duration: Duration) =
this.waitFor(duration.toMillis(), java.util.concurrent.TimeUnit.MILLISECONDS)

View File

@ -1,12 +0,0 @@
package net.corda.behave.database
import net.corda.behave.node.configuration.DatabaseConfiguration
open class DatabaseConfigurationTemplate {
open val connectionString: (DatabaseConfiguration) -> String = { "" }
protected open val config: (DatabaseConfiguration) -> String = { "" }
fun generate(config: DatabaseConfiguration) = config(config).trimMargin()
}

View File

@ -1,85 +0,0 @@
package net.corda.behave.database
import net.corda.behave.node.configuration.DatabaseConfiguration
import java.io.Closeable
import java.sql.*
import java.util.*
class DatabaseConnection(
private val config: DatabaseConfiguration,
template: DatabaseConfigurationTemplate
) : Closeable {
private val connectionString = template.connectionString(config)
private var conn: Connection? = null
fun open(): Connection {
try {
val connectionProps = Properties()
connectionProps["user"] = config.username
connectionProps["password"] = config.password
retry (5) {
conn = DriverManager.getConnection(connectionString, connectionProps)
}
return conn ?: throw Exception("Unable to open connection")
} catch (ex: SQLException) {
throw Exception("An error occurred whilst connecting to \"$connectionString\". " +
"Maybe the user and/or password is invalid?", ex)
}
}
override fun close() {
val connection = conn
if (connection != null) {
try {
conn = null
connection.close()
} catch (ex: SQLException) {
throw Exception("Failed to close database connection to \"$connectionString\"", ex)
}
}
}
private fun query(conn: Connection?, stmt: String? = null) {
var statement: Statement? = null
val resultset: ResultSet?
try {
statement = conn?.prepareStatement(stmt
?: "SELECT name FROM sys.tables WHERE name = ?")
statement?.setString(1, "Test")
resultset = statement?.executeQuery()
try {
while (resultset?.next() == true) {
val name = resultset.getString("name")
println(name)
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
resultset?.close()
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
statement?.close()
}
}
private fun retry(numberOfTimes: Int, action: () -> Unit) {
var i = numberOfTimes
while (numberOfTimes > 0) {
Thread.sleep(2000)
try {
action()
} catch (ex: Exception) {
if (i == 1) {
throw ex
}
}
i -= 1
}
}
}

View File

@ -1,66 +0,0 @@
package net.corda.behave.database
import net.corda.behave.node.configuration.Configuration
import net.corda.behave.node.configuration.DatabaseConfiguration
import net.corda.behave.service.Service
import net.corda.behave.service.ServiceInitiator
class DatabaseSettings {
var databaseName: String = "node"
private set
var schemaName: String = "dbo"
private set
var userName: String = "sa"
private set
var driverJar: String? = null
private set
private var databaseConfigTemplate: DatabaseConfigurationTemplate = DatabaseConfigurationTemplate()
private val serviceInitiators = mutableListOf<ServiceInitiator>()
fun withDatabase(name: String): DatabaseSettings {
databaseName = name
return this
}
fun withSchema(name: String): DatabaseSettings {
schemaName = name
return this
}
fun withDriver(name: String): DatabaseSettings {
driverJar = name
return this
}
fun withUser(name: String): DatabaseSettings {
userName = name
return this
}
fun withServiceInitiator(initiator: ServiceInitiator): DatabaseSettings {
serviceInitiators.add(initiator)
return this
}
fun withConfigTemplate(configTemplate: DatabaseConfigurationTemplate): DatabaseSettings {
databaseConfigTemplate = configTemplate
return this
}
fun config(config: DatabaseConfiguration): String {
return databaseConfigTemplate.generate(config)
}
fun dependencies(config: Configuration): List<Service> {
return serviceInitiators.map { it(config) }
}
val template: DatabaseConfigurationTemplate
get() = databaseConfigTemplate
}

View File

@ -1,48 +0,0 @@
package net.corda.behave.database
import net.corda.behave.database.configuration.H2ConfigurationTemplate
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.PostgreSQLService
enum class DatabaseType(val settings: DatabaseSettings) {
H2(DatabaseSettings()
.withDatabase(H2Service.database)
.withSchema(H2Service.schema)
.withUser(H2Service.username)
.withConfigTemplate(H2ConfigurationTemplate())
.withServiceInitiator {
H2Service("h2-${it.name}", it.database.port)
}
),
POSTGRES(DatabaseSettings()
.withDatabase(PostgreSQLService.database)
.withDriver(PostgreSQLService.driver)
.withSchema(PostgreSQLService.schema)
.withUser(PostgreSQLService.username)
.withConfigTemplate(PostgresConfigurationTemplate())
.withServiceInitiator {
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)
companion object {
fun fromName(name: String): DatabaseType? = when (name.toLowerCase()) {
"h2" -> H2
"postgres" -> POSTGRES
"postgresql" -> POSTGRES
else -> null
}
}
}

View File

@ -1,17 +0,0 @@
package net.corda.behave.database.configuration
import net.corda.behave.database.DatabaseConfigurationTemplate
import net.corda.behave.node.configuration.DatabaseConfiguration
class H2ConfigurationTemplate : DatabaseConfigurationTemplate() {
override val connectionString: (DatabaseConfiguration) -> String
get() = { "jdbc:h2:tcp://${it.host}:${it.port}/${it.database}" }
override val config: (DatabaseConfiguration) -> String
get() = {
"""
|h2port=${it.port}
"""
}
}

View File

@ -1,25 +0,0 @@
package net.corda.behave.database.configuration
import net.corda.behave.database.DatabaseConfigurationTemplate
import net.corda.behave.node.configuration.DatabaseConfiguration
class PostgresConfigurationTemplate : DatabaseConfigurationTemplate() {
override val connectionString: (DatabaseConfiguration) -> String
get() = { "jdbc:postgresql://${it.host}:${it.port}/${it.database}" }
override val config: (DatabaseConfiguration) -> String
get() = {
"""
|dataSourceProperties = {
| dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource"
| dataSource.url = "${connectionString(it)}"
| dataSource.user = "${it.username}"
| dataSource.password = "${it.password}"
|}
|database = {
| transactionIsolationLevel = READ_COMMITTED
|}
"""
}
}

View File

@ -1,11 +0,0 @@
package net.corda.behave.file
import java.nio.file.Path
import java.nio.file.Paths
val currentDirectory: Path
get() = Paths.get(System.getProperty("user.dir"))
// location of Corda distributions and Drivers dependencies
val stagingRoot: Path
get() = System.getProperty("STAGING_ROOT")?.let { Paths.get(it) } ?: currentDirectory

View File

@ -1,46 +0,0 @@
package net.corda.behave.file
import net.corda.core.internal.list
import net.corda.core.internal.readText
import java.nio.file.Path
import kotlin.streams.toList
class LogSource(
private val directory: Path,
filePattern: String? = ".*\\.log",
private val filePatternUsedForExclusion: Boolean = false
) {
private val fileRegex = Regex(filePattern ?: ".*")
data class MatchedLogContent(
val filename: Path,
val contents: String
)
fun find(pattern: String? = null): List<MatchedLogContent> {
val regex = if (pattern != null) {
Regex(pattern)
} else {
null
}
val logFiles = directory.list {
it.filter {
(!filePatternUsedForExclusion && it.fileName.toString().matches(fileRegex)) ||
(filePatternUsedForExclusion && !it.fileName.toString().matches(fileRegex))
}.toList()
}
val result = mutableListOf<MatchedLogContent>()
for (file in logFiles) {
val contents = file.readText()
if (regex != null) {
result.addAll(regex.findAll(contents).map { match ->
MatchedLogContent(file, match.value)
})
} else {
result.add(MatchedLogContent(file, contents))
}
}
return result
}
}

View File

@ -1,23 +0,0 @@
package net.corda.behave.monitoring
import net.corda.behave.await
import java.time.Duration
import java.util.concurrent.CountDownLatch
class ConjunctiveWatch(
private val left: Watch,
private val right: Watch
) : Watch {
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(timeout)) {
countDownLatch.countDown()
}
}
return countDownLatch.await(timeout)
}
}

View File

@ -1,24 +0,0 @@
package net.corda.behave.monitoring
import net.corda.behave.await
import java.time.Duration
import java.util.concurrent.CountDownLatch
class DisjunctiveWatch(
private val left: Watch,
private val right: Watch
) : Watch {
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(timeout)) {
countDownLatch.countDown()
}
}
return countDownLatch.await(timeout)
}
}

View File

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

View File

@ -1,52 +0,0 @@
package net.corda.behave.monitoring
import net.corda.behave.await
import net.corda.core.utilities.seconds
import rx.Observable
import java.time.Duration
import java.util.concurrent.CountDownLatch
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

@ -1,291 +0,0 @@
package net.corda.behave.network
import net.corda.behave.database.DatabaseType
import net.corda.behave.file.LogSource
import net.corda.behave.file.currentDirectory
import net.corda.behave.file.stagingRoot
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 net.corda.core.internal.*
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.minutes
import org.apache.commons.io.FileUtils
import java.io.Closeable
import java.nio.file.Path
import java.time.Duration
import java.time.Instant
import java.time.ZoneOffset.UTC
import java.time.format.DateTimeFormatter
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class Network private constructor(
private val nodes: Map<String, Node>,
private val targetDirectory: Path,
private val timeout: Duration = 2.minutes
) : Closeable, Iterable<Node> {
private val latch = CountDownLatch(1)
private var isRunning = false
private var isStopped = false
private var hasError = false
init {
targetDirectory.createDirectories()
}
class Builder internal constructor(
private val timeout: Duration
) {
private val nodes = mutableMapOf<String, Node>()
private val startTime = DateTimeFormatter
.ofPattern("yyyyMMdd-HHmmss")
.withZone(UTC)
.format(Instant.now())
private val directory = currentDirectory / "build" / "runs" / startTime
fun addNode(
name: String,
distribution: Distribution = Distribution.MASTER,
databaseType: DatabaseType = DatabaseType.H2,
notaryType: NotaryType = NotaryType.NONE,
issuableCurrencies: List<String> = emptyList()
): Builder {
return addNode(Node.new()
.withName(name)
.withDistribution(distribution)
.withDatabaseType(databaseType)
.withNotaryType(notaryType)
.withIssuableCurrencies(*issuableCurrencies.toTypedArray())
)
}
fun addNode(nodeBuilder: Node.Builder): Builder {
nodeBuilder
.withDirectory(directory)
.withTimeout(timeout)
val node = nodeBuilder.build()
nodes[node.config.name] = node
return this
}
fun generate(): Network {
val network = Network(nodes, directory, timeout)
network.copyDatabaseDrivers()
if (!network.configureNodes()) {
throw CordaException("Unable to configure nodes in Corda network. Please check logs in $directory")
}
network.bootstrapLocalNetwork()
return network
}
}
fun copyDatabaseDrivers() {
val driverDirectory = (targetDirectory / "libs").createDirectories()
log.info("Copying database drivers from $stagingRoot/drivers to $driverDirectory")
FileUtils.copyDirectory((stagingRoot / "drivers").toFile(), driverDirectory.toFile())
}
fun configureNodes(): Boolean {
var allDependenciesStarted = true
log.info("Configuring nodes ...")
for (node in nodes.values) {
node.configure()
if (!node.startDependencies()) {
allDependenciesStarted = false
break
}
}
return if (allDependenciesStarted) {
log.info("Nodes configured")
true
} else {
false
}
}
private fun bootstrapLocalNetwork() {
val bootstrapper = nodes.values
.sortedByDescending { it.config.distribution.version }
.first()
.config.distribution.networkBootstrapper
if (!bootstrapper.exists()) {
signalFailure("Network bootstrapping tool does not exist; exiting ...")
return
}
log.info("Bootstrapping network, please wait ...")
val command = JarCommand(
bootstrapper,
arrayOf("$targetDirectory"),
targetDirectory,
timeout
)
log.info("Running command: {}", command)
command.output.subscribe {
if (it.contains("Exception")) {
log.warn("Found error in output; interrupting bootstrapping action ...\n{}", it)
command.interrupt()
}
}
command.start()
if (!command.waitFor()) {
hasError = true
error("Failed to bootstrap network") {
val matches = LogSource(targetDirectory)
.find(".*[Ee]xception.*")
.groupBy { it.filename.toAbsolutePath() }
for ((key, value) in matches) {
log.info("Log($key):\n${value.joinToString("\n") { it.contents }}")
}
}
} else {
log.info("Network set-up completed")
}
}
private fun cleanup() {
try {
if (!hasError || CLEANUP_ON_ERROR) {
log.info("Cleaning up runtime ...")
targetDirectory.deleteRecursively()
} else {
log.info("Deleting temporary files, but retaining logs and config ...")
for (node in nodes.values) {
val nodeDir = targetDirectory / node.config.name
nodeDir.list { paths -> paths
.filter { it.fileName.toString() !in setOf("logs", "node.conf") }
.forEach(Path::deleteRecursively)
}
}
listOf("libs", ".cache").forEach { (targetDirectory / it).deleteRecursively() }
}
log.info("Network was shut down successfully")
} catch (e: Exception) {
log.warn("Failed to cleanup runtime environment")
e.printStackTrace()
}
}
private fun error(message: String, ex: Throwable? = null, action: (() -> Unit)? = null) {
hasError = true
log.warn(message, ex)
action?.invoke()
stop()
throw Exception(message, ex)
}
fun start() {
if (isRunning || hasError) {
return
}
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
}
var failedNodes = 0
val nodesLatch = CountDownLatch(nodes.size)
nodes.values.parallelStream().forEach {
if (!it.waitUntilRunning(waitDuration ?: timeout)) {
failedNodes += 1
}
nodesLatch.countDown()
}
nodesLatch.await()
return if (failedNodes > 0) {
error("$failedNodes node(s) did not start up as expected within the given time frame") {
signal()
keepAlive(timeout)
}
false
} else {
log.info("All nodes are running")
true
}
}
fun signalFailure(message: String?, ex: Throwable? = null) {
error(message ?: "Signaling error to network ...", ex) {
signal()
keepAlive(timeout)
}
}
fun signal() {
log.info("Sending termination signal ...")
latch.countDown()
}
fun keepAlive(timeout: Duration) {
val secs = timeout.seconds
log.info("Waiting for up to {} second(s) for termination signal ...", secs)
val wasSignalled = latch.await(secs, TimeUnit.SECONDS)
log.info(if (wasSignalled) {
"Received termination signal"
} else {
"Timed out. No termination signal received during wait period"
})
stop()
}
fun stop() {
if (isStopped) {
return
}
log.info("Shutting down network ...")
isStopped = true
log.info("Shutting down nodes ...")
for (node in nodes.values) {
node.shutDown()
}
cleanup()
}
fun use(action: (Network) -> Unit) {
this.start()
action(this)
close()
}
override fun close() {
stop()
}
override fun iterator(): Iterator<Node> {
return nodes.values.iterator()
}
operator fun get(nodeName: String): Node? {
return nodes[nodeName]
}
companion object {
val log = contextLogger()
const val CLEANUP_ON_ERROR = false
fun new(timeout: Duration = 2.minutes
): Builder = Builder(timeout)
}
}

View File

@ -1,136 +0,0 @@
package net.corda.behave.node
import net.corda.behave.file.stagingRoot
import net.corda.core.internal.copyTo
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.core.utilities.contextLogger
import java.net.URL
import java.nio.file.Path
/**
* Corda distribution.
*/
class Distribution private constructor(
/**
* The version string of the Corda distribution.
*/
val version: String,
/**
* The path of the distribution fat JAR on disk, if available.
*/
file: Path? = null,
/**
* The URL of the distribution fat JAR, if available.
*/
val url: URL? = null,
/**
* The Docker image details, if available
*/
val baseImage: String? = null
) {
/**
* The path to the distribution fat JAR.
*/
val path: Path = file ?: nodePrefix / version
/**
* The path to the distribution fat JAR.
*/
val cordaJar: Path = path / "corda.jar"
/**
* The path to available Cordapps for this distribution.
*/
val cordappDirectory: Path = path / "apps"
/**
* The path to network bootstrapping tool.
*/
val networkBootstrapper: Path = path / "network-bootstrapper.jar"
/**
* Ensure that the distribution is available on disk.
*/
fun ensureAvailable() {
if (cordaJar.exists()) return
val url = checkNotNull(url) { "File not found $cordaJar" }
try {
cordaJar.parent.createDirectories()
url.openStream().use { it.copyTo(cordaJar) }
} catch (e: Exception) {
if ("HTTP response code: 401" in e.message!!) {
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.")
}
throw e
}
}
/**
* Human-readable representation of the distribution.
*/
override fun toString() = "Corda(version = $version, path = $cordaJar)"
companion object {
private val log = contextLogger()
private val distributions = mutableListOf<Distribution>()
private val nodePrefix = stagingRoot / "corda"
val MASTER = fromJarFile("corda-master")
/**
* Get representation of a Corda distribution from Artifactory based on its version string.
* @param version The version of the Corda distribution.
*/
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
}
/**
* Get representation of a Corda distribution based on its version string and fat JAR path.
* @param version The version of the Corda distribution.
* @param jarFile The path to the Corda fat JAR.
*/
fun fromJarFile(version: String, jarFile: Path? = null): Distribution {
val distribution = Distribution(version, file = jarFile)
distributions.add(distribution)
return distribution
}
/**
* Get Corda distribution from a Docker image file.
* @param baseImage The name (eg. corda) of the Corda distribution.
* @param imageTag The version (github commit id or corda version) of the Corda distribution.
*/
fun fromDockerImage(baseImage: String, imageTag: String): Distribution {
val distribution = Distribution(version = imageTag, baseImage = baseImage)
distributions.add(distribution)
return distribution
}
/**
* Get registered representation of a Corda distribution based on its version string.
* @param version The version of the Corda distribution
*/
fun fromVersionString(version: String): Distribution = when (version) {
"master" -> MASTER
"corda-3.0" -> fromArtifactory(version)
else -> fromJarFile(version)
}
}
}

View File

@ -1,347 +0,0 @@
package net.corda.behave.node
import net.corda.behave.database.DatabaseConnection
import net.corda.behave.database.DatabaseType
import net.corda.behave.file.LogSource
import net.corda.behave.file.currentDirectory
import net.corda.behave.file.stagingRoot
import net.corda.behave.monitoring.PatternWatch
import net.corda.behave.node.configuration.*
import net.corda.behave.process.JarCommand
import net.corda.behave.service.Service
import net.corda.behave.service.ServiceSettings
import net.corda.behave.ssh.MonitoringSSHClient
import net.corda.behave.ssh.SSHClient
import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.seconds
import org.apache.commons.io.FileUtils
import java.net.InetAddress
import java.nio.file.Path
import java.time.Duration
import java.util.concurrent.CountDownLatch
/**
* Corda node.
*/
class Node(
val config: Configuration,
private val rootDirectory: Path = currentDirectory,
private val settings: ServiceSettings = ServiceSettings()
) {
private val log = loggerFor<Node>()
private val runtimeDirectory = rootDirectory / config.name
private val logDirectory = runtimeDirectory / "logs"
private val command = JarCommand(
config.distribution.cordaJar,
arrayOf("--config", "node.conf"),
runtimeDirectory,
settings.timeout,
enableRemoteDebugging = false
)
private val isAliveLatch = PatternWatch(command.output, "Node for \".*\" started up and registered")
private var isConfigured = false
private val serviceDependencies = mutableListOf<Service>()
private var isStarted = false
private var haveDependenciesStarted = false
private var haveDependenciesStopped = false
fun describe(): String {
val network = config.nodeInterface
val database = config.database
return """
|Node Information: ${config.name}
| - P2P: ${network.host}:${network.p2pPort}
| - RPC: ${network.host}:${network.rpcPort}
| - SSH: ${network.host}:${network.sshPort}
| - DB: ${network.host}:${database.port} (${database.type})
|""".trimMargin()
}
fun configure() {
if (isConfigured) { return }
isConfigured = true
log.info("Configuring {} ...", this)
serviceDependencies.addAll(config.database.type.dependencies(config))
config.distribution.ensureAvailable()
config.writeToFile(rootDirectory / "${config.name}_node.conf")
installApps()
}
fun start(): Boolean {
if (!startDependencies()) {
return false
}
log.info("Starting {} ...", this)
return try {
command.start()
isStarted = true
true
} catch (e: Exception) {
log.warn("Failed to start {}", this)
e.printStackTrace()
false
}
}
fun waitUntilRunning(waitDuration: Duration? = null): Boolean {
val ok = isAliveLatch.await(waitDuration ?: settings.timeout)
if (!ok) {
log.warn("{} did not start up as expected within the given time frame", this)
} else {
log.info("{} is running and ready for incoming connections", this)
}
return ok
}
fun shutDown(): Boolean {
return try {
if (isStarted) {
log.info("Shutting down {} ...", this)
command.kill()
}
stopDependencies()
true
} catch (e: Exception) {
log.warn("Failed to shut down {} cleanly", this)
e.printStackTrace()
false
}
}
val nodeInfoGenerationOutput: LogSource by lazy {
LogSource(logDirectory, "node-info-gen.log")
}
val logOutput: LogSource by lazy {
val hostname = InetAddress.getLocalHost().hostName
LogSource(logDirectory, "node-$hostname.*.log")
}
val database: DatabaseConnection by lazy {
DatabaseConnection(config.database, config.databaseType.settings.template)
}
fun ssh(
exitLatch: CountDownLatch? = null,
clientLogic: (MonitoringSSHClient) -> Unit
) {
Thread(Runnable {
val network = config.nodeInterface
val user = config.users.first()
val client = SSHClient.connect(network.sshPort, user.password, username = user.username)
MonitoringSSHClient(client).use {
log.info("Connected to {} over SSH", this)
clientLogic(it)
log.info("Disconnecting from {} ...", this)
it.writeLine("bye")
exitLatch?.countDown()
}
}).start()
}
fun <T> rpc(action: (CordaRPCOps) -> T): T {
var result: T? = null
val user = config.users.first()
val address = config.nodeInterface
val targetHost = NetworkHostAndPort(address.host, address.rpcPort)
val config = CordaRPCClientConfiguration.DEFAULT.copy(
connectionMaxRetryInterval = 10.seconds
)
log.info("Establishing RPC connection to ${targetHost.host} on port ${targetHost.port} ...")
CordaRPCClient(targetHost, config).use(user.username, user.password) {
log.info("RPC connection to ${targetHost.host}:${targetHost.port} established")
val client = it.proxy
result = action(client)
}
return result ?: error("Failed to run RPC action")
}
override fun toString(): String {
return "Node(name = ${config.name}, version = ${config.distribution.version})"
}
fun startDependencies(): Boolean {
if (haveDependenciesStarted) { return true }
haveDependenciesStarted = true
if (serviceDependencies.isEmpty()) { return true }
log.info("Starting dependencies for {} ...", this)
val latch = CountDownLatch(serviceDependencies.size)
var failed = false
serviceDependencies.parallelStream().forEach {
val wasStarted = it.start()
latch.countDown()
if (!wasStarted) {
failed = true
}
}
latch.await()
return if (!failed) {
log.info("Dependencies started for {}", this)
true
} else {
log.warn("Failed to start one or more dependencies for {}", this)
false
}
}
private fun stopDependencies() {
if (haveDependenciesStopped) { return }
haveDependenciesStopped = true
if (serviceDependencies.isEmpty()) { return }
log.info("Stopping dependencies for {} ...", this)
val latch = CountDownLatch(serviceDependencies.size)
serviceDependencies.parallelStream().forEach {
it.stop()
latch.countDown()
}
latch.await()
log.info("Dependencies stopped for {}", this)
}
private fun installApps() {
val version = config.distribution.version
val appDirectory = stagingRoot / "corda" / version / "apps"
if (appDirectory.exists()) {
val targetAppDirectory = runtimeDirectory / "cordapps"
FileUtils.copyDirectory(appDirectory.toFile(), targetAppDirectory.toFile())
}
}
class Builder {
var name: String? = null
private set
private var distribution = Distribution.MASTER
private var databaseType = DatabaseType.H2
private var notaryType = NotaryType.NONE
private val issuableCurrencies = mutableListOf<String>()
private var location: String = "London"
private var country: String = "GB"
private val apps = mutableListOf<String>()
private var includeFinance = false
private var directory: Path? = null
private var timeout = Duration.ofSeconds(60)
fun withName(newName: String): Builder {
name = newName
return this
}
fun withDistribution(newDistribution: Distribution): Builder {
distribution = newDistribution
return this
}
fun withDatabaseType(newDatabaseType: DatabaseType): Builder {
databaseType = newDatabaseType
return this
}
fun withNotaryType(newNotaryType: NotaryType): Builder {
notaryType = newNotaryType
return this
}
fun withIssuableCurrencies(vararg currencies: String): Builder {
issuableCurrencies.addAll(currencies)
return this
}
fun withIssuableCurrencies(currencies: List<String>): Builder {
issuableCurrencies.addAll(currencies)
return this
}
fun withLocation(location: String, country: String): Builder {
this.location = location
this.country = country
return this
}
fun withFinanceApp(): Builder {
includeFinance = true
return this
}
fun withApp(app: String): Builder {
apps.add(app)
return this
}
fun withDirectory(newDirectory: Path): Builder {
directory = newDirectory
return this
}
fun withTimeout(newTimeout: Duration): Builder {
timeout = newTimeout
return this
}
fun build(): Node {
val name = name ?: error("Node name not set")
val directory = directory ?: error("Runtime directory not set")
return Node(
Configuration(
name,
distribution,
databaseType,
location = location,
country = country,
notary = NotaryConfiguration(notaryType),
cordapps = CordappConfiguration(
apps = apps,
includeFinance = includeFinance
),
configElements = *arrayOf(
NotaryConfiguration(notaryType),
CurrencyConfiguration(issuableCurrencies)
)
),
directory,
ServiceSettings(timeout)
)
}
private fun <T> error(message: String): T {
throw IllegalArgumentException(message)
}
}
companion object {
fun new() = Builder()
}
}

View File

@ -1,58 +0,0 @@
package net.corda.behave.node.configuration
import net.corda.behave.database.DatabaseType
import net.corda.behave.node.Distribution
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.writeText
import net.corda.core.utilities.contextLogger
import java.nio.file.Path
class Configuration(
val name: String,
val distribution: Distribution = Distribution.MASTER,
val databaseType: DatabaseType = DatabaseType.H2,
val location: String = "London",
val country: String = "GB",
val users: UserConfiguration = UserConfiguration().withUser("corda", DEFAULT_PASSWORD),
val nodeInterface: NetworkInterface = NetworkInterface(),
val database: DatabaseConfiguration = DatabaseConfiguration(
databaseType,
nodeInterface.host,
nodeInterface.dbPort,
password = DEFAULT_PASSWORD
),
// TODO This is not being used when it could be. The call-site is using configElements instead.
val notary: NotaryConfiguration = NotaryConfiguration(),
val cordapps: CordappConfiguration = CordappConfiguration(),
vararg configElements: ConfigurationTemplate
) {
private val developerMode = true
val cordaX500Name: CordaX500Name = CordaX500Name(name, location, country)
private val basicConfig = """
|myLegalName="C=$country,L=$location,O=$name"
|keyStorePassword="cordacadevpass"
|trustStorePassword="trustpass"
|devMode=$developerMode
|jarDirs = [ "../libs" ]
""".trimMargin()
private val extraConfig = (configElements.toList() + listOf(users, nodeInterface))
.joinToString(separator = "\n") { it.generate(this) }
fun writeToFile(file: Path) {
file.writeText(this.generate())
log.debug(this.generate())
}
private fun generate() = listOf(basicConfig, database.config(), extraConfig)
.filter { it.isNotBlank() }
.joinToString("\n")
companion object {
private val log = contextLogger()
const val DEFAULT_PASSWORD = "S0meS3cretW0rd"
}
}

View File

@ -1,8 +0,0 @@
package net.corda.behave.node.configuration
open class ConfigurationTemplate {
protected open val config: (Configuration) -> String = { "" }
fun generate(config: Configuration) = config(config).trimMargin()
}

View File

@ -1,29 +0,0 @@
package net.corda.behave.node.configuration
// TODO This is a ConfigurationTemplate but is never used as one. Therefore the private "applications" list is never used
// and thus includeFinance isn't necessary either. Something is amiss.
class CordappConfiguration(var apps: List<String> = emptyList(), val includeFinance: Boolean = false) : ConfigurationTemplate() {
private val applications = apps + if (includeFinance) {
listOf("net.corda:corda-finance:CORDA_VERSION")
} else {
emptyList()
}
override val config: (Configuration) -> String
get() = { config ->
if (applications.isEmpty()) {
""
} else {
"""
|cordapps = [
|${applications.joinToString(", ") { formatApp(config, it) }}
|]
"""
}
}
private fun formatApp(config: Configuration, app: String): String {
return "\"${app.replace("CORDA_VERSION", config.distribution.version)}\""
}
}

View File

@ -1,23 +0,0 @@
package net.corda.behave.node.configuration
class CurrencyConfiguration(private val issuableCurrencies: List<String>) : ConfigurationTemplate() {
override val config: (Configuration) -> String
get() = {
if (issuableCurrencies.isEmpty()) {
""
} else {
// TODO This is no longer correct. issuableCurrencies is a config of the finance app and belongs
// in a separate .conf file for the app (in the config sub-directory, with a filename matching the CorDapp
// jar filename). It is no longer read in from the node conf file. There seem to be pieces missing in the
// behave framework to allow one to do this.
"""
|custom : {
| issuableCurrencies : [
| ${issuableCurrencies.joinToString(", ")}
| ]
|}
"""
}
}
}

View File

@ -1,16 +0,0 @@
package net.corda.behave.node.configuration
import net.corda.behave.database.DatabaseType
data class DatabaseConfiguration(
val type: DatabaseType,
val host: String,
val port: Int,
val username: String = type.settings.userName,
val password: String,
val database: String = type.settings.databaseName,
val schema: String = type.settings.schemaName
) {
fun config() = type.settings.config(this)
}

View File

@ -1,62 +0,0 @@
package net.corda.behave.node.configuration
import java.net.Socket
import java.util.concurrent.atomic.AtomicInteger
data class NetworkInterface(
val host: String = "localhost",
val sshPort: Int = getPort(2222 + nodeIndex),
val p2pPort: Int = getPort(12001 + (nodeIndex * 5)),
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 dockerPort: Int = getPort(5000 + (nodeIndex * 5))
) : ConfigurationTemplate() {
init {
nodeIndex += 1
}
override val config: (Configuration) -> String
get() = {
"""
|sshd={ port=$sshPort }
|p2pAddress="$host:$p2pPort"
|rpcSettings = {
| useSsl = false
| standAloneBroker = false
| address = "$host:$rpcPort"
| adminAddress = "$host:$rpcAdminPort"
|}
"""
}
companion object {
private var nodeIndex = 0
private var startOfBackupRange = AtomicInteger(40000)
private fun getPort(suggestedPortNumber: Int): Int {
var portNumber = suggestedPortNumber
while (isPortInUse(portNumber)) {
portNumber = startOfBackupRange.getAndIncrement()
}
if (portNumber >= 65535) {
throw Exception("No free port found (suggested $suggestedPortNumber)")
}
return portNumber
}
private fun isPortInUse(portNumber: Int): Boolean {
return try {
val s = Socket("localhost", portNumber)
s.close()
true
} catch (_: Exception) {
false
}
}
}
}

View File

@ -1,15 +0,0 @@
package net.corda.behave.node.configuration
class NotaryConfiguration(private val notaryType: NotaryType = NotaryType.NONE) : ConfigurationTemplate() {
override val config: (Configuration) -> String
get() = {
when (notaryType) {
NotaryType.NONE -> ""
NotaryType.NON_VALIDATING ->
"notary { validating = false }"
NotaryType.VALIDATING ->
"notary { validating = true }"
}
}
}

View File

@ -1,17 +0,0 @@
package net.corda.behave.node.configuration
enum class NotaryType {
NONE,
VALIDATING,
NON_VALIDATING
}
fun String.toNotaryType(): NotaryType? {
return when (this.toLowerCase()) {
"non-validating" -> NotaryType.NON_VALIDATING
"nonvalidating" -> NotaryType.NON_VALIDATING
"validating" -> NotaryType.VALIDATING
else -> null
}
}

View File

@ -1,36 +0,0 @@
package net.corda.behave.node.configuration
class UserConfiguration : ConfigurationTemplate(), Iterable<UserConfiguration.User> {
data class User(val username: String, val password: String, val permissions: List<String>)
private val users = mutableListOf<User>()
fun withUser(username: String, password: String, permissions: List<String> = listOf("ALL")): UserConfiguration {
users.add(User(username, password, permissions))
return this
}
override fun iterator(): Iterator<User> {
return users.iterator()
}
override val config: (Configuration) -> String
get() = {
"""
|rpcUsers=[
|${users.joinToString("\n") { userObject(it) }}
|]
"""
}
private fun userObject(user: User): String {
return """
|{
| username="${user.username}"
| password="${user.password}"
| permissions=[${user.permissions.joinToString(", ")}]
|}
""".trimMargin()
}
}

View File

@ -1,156 +0,0 @@
package net.corda.behave.process
import net.corda.behave.await
import net.corda.behave.file.currentDirectory
import net.corda.behave.process.output.OutputListener
import net.corda.behave.waitFor
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds
import rx.Observable
import rx.Subscriber
import java.io.Closeable
import java.io.IOException
import java.nio.file.Path
import java.time.Duration
import java.util.concurrent.CountDownLatch
open class Command(
private val command: List<String>,
private val directory: Path = currentDirectory,
private val timeout: Duration = 2.minutes
): Closeable {
companion object {
private val WAIT_BEFORE_KILL: Duration = 5.seconds
private val log = contextLogger()
}
private val terminationLatch = CountDownLatch(1)
private val outputCapturedLatch = CountDownLatch(1)
private var isInterrupted = false
private var process: Process? = null
private var outputListener: OutputListener? = null
var exitCode = -1
private set
val output: Observable<String> = Observable.create<String> { emitter ->
outputListener = object : OutputListener {
override fun onNewLine(line: String) = emitter.onNext(line)
override fun onEndOfStream() = emitter.onCompleted()
}
}.share()
private val thread = Thread(Runnable {
try {
log.info("Executing command: $command from directory: $directory")
val processBuilder = ProcessBuilder(command)
.directory(directory.toFile())
.redirectErrorStream(true)
processBuilder.environment().putAll(System.getenv())
process = processBuilder.start()
val process = process!!
Thread(Runnable {
val input = process.inputStream.bufferedReader()
while (true) {
try {
val line = input.readLine()?.trimEnd() ?: break
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()
outputCapturedLatch.countDown()
}).start()
val streamIsClosed = outputCapturedLatch.await(timeout)
val timeout = if (!streamIsClosed || isInterrupted) {
1.seconds
} else {
timeout
}
if (!process.waitFor(timeout)) {
process.destroy()
process.waitFor(WAIT_BEFORE_KILL)
if (process.isAlive) {
process.destroyForcibly()
process.waitFor()
}
}
exitCode = process.exitValue()
if (isInterrupted) {
log.warn("Process ended after interruption")
} else if (exitCode != 0 && exitCode != 143 /* SIGTERM */) {
log.warn("Process {} ended with exit code {}", this, exitCode)
}
} catch (e: Exception) {
log.warn("Error occurred when trying to run process", e)
throw e
}
finally {
process = null
terminationLatch.countDown()
}
})
fun start() {
thread.start()
}
fun interrupt() {
isInterrupted = true
outputCapturedLatch.countDown()
}
fun waitFor(): Boolean {
terminationLatch.await()
return exitCode == 0
}
fun kill() {
process?.destroy()
process?.waitFor(WAIT_BEFORE_KILL)
if (process?.isAlive == true) {
process?.destroyForcibly()
}
if (process != null) {
terminationLatch.await()
}
process = null
}
override fun close() {
if (process?.isAlive == true) {
kill()
}
waitFor()
}
fun run(action: (Command) -> Unit = { }): Int {
use {
start()
action(this)
}
return exitCode
}
fun run(subscriber: Subscriber<String>, action: (Command, Observable<String>) -> Unit = { _, _ -> }): Int {
run {
output.subscribe(subscriber)
start()
action(this, output)
}
return exitCode
}
override fun toString() = "Command(${command.joinToString(" ")})"
}

View File

@ -1,32 +0,0 @@
package net.corda.behave.process
import java.nio.file.Path
import java.time.Duration
class JarCommand(
jarFile: Path,
arguments: Array<out String>,
directory: Path,
timeout: Duration,
enableRemoteDebugging: Boolean = false
) : Command(
command = listOf(
"/usr/bin/java",
*extraArguments(enableRemoteDebugging),
"-jar", "$jarFile",
*arguments
),
directory = directory,
timeout = timeout
) {
companion object {
private fun extraArguments(enableRemoteDebugging: Boolean) =
if (enableRemoteDebugging) {
arrayOf("-Dcapsule.jvm.args=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005")
} else {
arrayOf()
}
}
}

View File

@ -1,8 +0,0 @@
package net.corda.behave.process.output
interface OutputListener {
fun onNewLine(line: String)
fun onEndOfStream()
}

View File

@ -1,117 +0,0 @@
package net.corda.behave.service
import com.spotify.docker.client.DefaultDockerClient
import com.spotify.docker.client.DockerClient
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 rx.Observable
import java.io.Closeable
abstract class ContainerService(
name: String,
port: Int,
val startupStatement: String,
settings: ServiceSettings = ServiceSettings()
) : Service(name, port, settings), Closeable {
protected val client: DockerClient = DefaultDockerClient.fromEnv().build()
protected var id: String? = null
protected open val baseImage: String = ""
protected open val imageTag: String = "latest"
protected abstract val internalPort: Int
private var isClientOpen: Boolean = true
private val environmentVariables: MutableList<String> = mutableListOf()
private val imageReference: String
get() = "$baseImage:$imageTag"
override fun startService(): Boolean {
return try {
val port = "$internalPort"
val portBindings = mapOf(
port to listOf(PortBinding.of("0.0.0.0", this.port))
)
val hostConfig = HostConfig.builder().portBindings(portBindings).build()
val containerConfig = ContainerConfig.builder()
.hostConfig(hostConfig)
.image(imageReference)
.exposedPorts(port)
.env(*environmentVariables.toTypedArray())
.build()
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
e.printStackTrace()
false
}
}
override fun stopService(): Boolean {
if (id != null) {
client.stopContainer(id, 30)
client.removeContainer(id)
id = null
}
return true
}
protected fun addEnvironmentVariable(name: String, value: String) {
environmentVariables.add("$name=$value")
}
override fun checkPrerequisites() {
if (!client.listImages().any { true == it.repoTags()?.contains(imageReference) }) {
log.info("Pulling image $imageReference ...")
client.pull(imageReference) { }
log.info("Image $imageReference downloaded")
}
}
override fun verify(): Boolean {
return true
}
override fun waitUntilStarted(): Boolean {
try {
var timeout = settings.startupTimeout.toMillis()
while (timeout > 0) {
client.logs(id, DockerClient.LogsParam.stdout(), DockerClient.LogsParam.stderr()).use {
val contents = it.readFully()
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
}
}
timeout -= settings.pollInterval.toMillis()
}
return false
} catch (e: Exception) {
e.printStackTrace()
return false
}
}
override fun close() {
if (isClientOpen) {
isClientOpen = false
client.close()
}
}
}

View File

@ -1,71 +0,0 @@
package net.corda.behave.service
import net.corda.core.utilities.loggerFor
import java.io.Closeable
abstract class Service(
val name: String,
val port: Int,
val settings: ServiceSettings = ServiceSettings()
) : Closeable {
private var isRunning: Boolean = false
protected val log = loggerFor<Service>()
fun start(): Boolean {
if (isRunning) {
log.warn("{} is already running", this)
return false
}
log.info("Starting {} ...", this)
checkPrerequisites()
if (!startService()) {
log.warn("Failed to start {}", this)
return false
}
isRunning = true
Thread.sleep(settings.startupDelay.toMillis())
return if (!waitUntilStarted()) {
log.warn("Failed to start {}", this)
stop()
false
} else if (!verify()) {
log.warn("Failed to verify start-up of {}", this)
stop()
false
} else {
log.info("{} started and available", this)
true
}
}
fun stop() {
if (!isRunning) {
return
}
log.info("Stopping {} ...", this)
if (stopService()) {
log.info("{} stopped", this)
isRunning = false
} else {
log.warn("Failed to stop {}", this)
}
}
override fun close() {
stop()
}
override fun toString() = "Service(name = $name, port = $port)"
protected open fun checkPrerequisites() {}
protected open fun startService() = true
protected open fun stopService() = true
protected open fun verify() = true
protected open fun waitUntilStarted() = true
}

View File

@ -1,5 +0,0 @@
package net.corda.behave.service
import net.corda.behave.node.configuration.Configuration
typealias ServiceInitiator = (Configuration) -> Service

View File

@ -1,12 +0,0 @@
package net.corda.behave.service
import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds
import java.time.Duration
data class ServiceSettings(
val timeout: Duration = 1.minutes,
val startupDelay: Duration = 1.seconds,
val startupTimeout: Duration = 15.seconds,
val pollInterval: Duration = 1.seconds
)

View File

@ -1,16 +0,0 @@
package net.corda.behave.service.database
import net.corda.behave.service.Service
class H2Service(
name: String,
port: Int
) : Service(name, port) {
companion object {
const val database = "node"
const val schema = "dbo"
const val username = "sa"
}
}

View File

@ -1,50 +0,0 @@
package net.corda.behave.service.database
import net.corda.behave.database.DatabaseConnection
import net.corda.behave.database.DatabaseType
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 PostgreSQLService(
name: String,
port: Int,
private val password: String,
settings: ServiceSettings = ServiceSettings()
) : ContainerService(name, port, "database system is ready to accept connections", settings) {
override val baseImage = "postgres"
override val internalPort = 5432
override fun verify(): Boolean {
val config = DatabaseConfiguration(
type = DatabaseType.POSTGRES,
host = host,
port = port,
database = database,
schema = schema,
username = username,
password = password
)
val connection = DatabaseConnection(config, PostgresConfigurationTemplate())
try {
connection.use {
return true
}
} catch (ex: Exception) {
log.warn(ex.message, ex)
ex.printStackTrace()
}
return false
}
companion object {
const val host = "localhost"
const val database = "postgres"
const val schema = "public"
const val username = "postgres"
const val driver = "postgresql-42.1.4.jar"
}
}

View File

@ -1,66 +0,0 @@
package net.corda.behave.ssh
import net.corda.behave.process.output.OutputListener
import rx.Observable
import java.io.Closeable
import java.io.InterruptedIOException
class MonitoringSSHClient(
private val client: SSHClient
) : Closeable {
private var isRunning = false
private lateinit var outputListener: OutputListener
val output: Observable<String> = Observable.create<String> { emitter ->
outputListener = object : OutputListener {
override fun onNewLine(line: String) {
emitter.onNext(line)
}
override fun onEndOfStream() {
emitter.onCompleted()
}
}
}
private val thread = Thread(Runnable {
while (isRunning) {
try {
val line = client.readLine() ?: break
outputListener.onNewLine(line)
} catch (_: InterruptedIOException) {
break
}
}
outputListener.onEndOfStream()
})
init {
isRunning = true
output.subscribe()
thread.start()
}
override fun close() {
isRunning = false
thread.join(1000)
if (thread.isAlive) {
thread.interrupt()
}
client.close()
}
fun use(action: (MonitoringSSHClient) -> Unit) {
use {
action(this)
}
}
fun write(vararg bytes: Byte) = client.write(*bytes)
fun write(charSequence: CharSequence) = client.write(charSequence)
fun writeLine(string: String) = client.writeLine(string)
}

View File

@ -1,161 +0,0 @@
package net.corda.behave.ssh
import net.corda.core.utilities.contextLogger
import org.apache.sshd.client.SshClient
import org.apache.sshd.client.channel.ChannelShell
import org.apache.sshd.client.session.ClientSession
import org.apache.sshd.common.channel.SttySupport
import org.crsh.util.Utils
import java.io.*
import java.time.Duration
import java.util.concurrent.TimeUnit
open class SSHClient private constructor(
private val client: SshClient,
private val outputStream: OutputStream,
private val inputStream: InputStream,
private val session: ClientSession,
private val channel: ChannelShell
) : Closeable {
private var isClosed = false
fun read(): Int? {
if (isClosed) {
return null
}
val char = inputStream.read()
return if (char != -1) {
char
} else {
null
}
}
fun readLine(): String? {
if (isClosed) {
return null
}
var ch: Int?
val lineBuffer = mutableListOf<Char>()
while (true) {
ch = read()
if (ch == null) {
if (lineBuffer.isEmpty()) {
return null
}
break
}
lineBuffer.add(ch.toChar())
if (ch == 10) {
break
}
}
return String(lineBuffer.toCharArray())
}
fun write(s: CharSequence) {
if (isClosed) {
return
}
write(*s.toString().toByteArray(UTF8))
}
fun write(vararg bytes: Byte) {
if (isClosed) {
return
}
outputStream.write(bytes)
}
fun writeLine(s: String) {
write("$s\n")
flush()
}
fun flush() {
if (isClosed) {
return
}
outputStream.flush()
}
override fun close() {
if (isClosed) {
return
}
try {
Utils.close(outputStream)
channel.close(false)
session.close(false)
client.stop()
} finally {
isClosed = true
}
}
companion object {
private val log = contextLogger()
fun connect(
port: Int,
password: String,
hostname: String = "localhost",
username: String = "corda",
timeout: Duration = Duration.ofSeconds(4)
): SSHClient {
val tty = SttySupport.parsePtyModes(TTY)
val client = SshClient.setUpDefaultClient()
client.start()
log.info("Connecting to $hostname:$port ...")
val session = client
.connect(username, hostname, port)
.verify(timeout.seconds, TimeUnit.SECONDS)
.session
log.info("Authenticating using password identity ...")
session.addPasswordIdentity(password)
val authFuture = session.auth().verify(timeout.seconds, TimeUnit.SECONDS)
authFuture.addListener {
log.info("Authentication completed with " + if (it.isSuccess) "success" else "failure")
}
val channel = session.createShellChannel()
channel.ptyModes = tty
val outputStream = PipedOutputStream()
val channelIn = PipedInputStream(outputStream)
val channelOut = PipedOutputStream()
val inputStream = PipedInputStream(channelOut)
channel.`in` = channelIn
channel.out = channelOut
channel.err = ByteArrayOutputStream()
channel.open()
return SSHClient(client, outputStream, inputStream, session, channel)
}
private const val TTY = "speed 9600 baud; 36 rows; 180 columns;\n" +
"lflags: icanon isig iexten echo echoe -echok echoke -echonl echoctl\n" +
"\t-echoprt -altwerase -noflsh -tostop -flusho pendin -nokerninfo\n" +
"\t-extproc\n" +
"iflags: -istrip icrnl -inlcr -igncr ixon -ixoff ixany imaxbel iutf8\n" +
"\t-ignbrk brkint -inpck -ignpar -parmrk\n" +
"oflags: opost onlcr -oxtabs -onocr -onlret\n" +
"cflags: cread cs8 -parenb -parodd hupcl -clocal -cstopb -crtscts -dsrflow\n" +
"\t-dtrflow -mdmbuf\n" +
"cchars: discard = ^O; dsusp = ^Y; eof = ^D; eol = <undef>;\n" +
"\teol2 = <undef>; erase = ^?; intr = ^C; kill = ^U; lnext = ^V;\n" +
"\tmin = 1; quit = ^\\; reprint = ^R; start = ^Q; status = ^T;\n" +
"\tstop = ^S; susp = ^Z; time = 0; werase = ^W;"
private val UTF8 = charset("UTF-8")
}
}

View File

@ -1,11 +0,0 @@
import cucumber.api.CucumberOptions
import cucumber.api.junit.Cucumber
import org.junit.runner.RunWith
@RunWith(Cucumber::class)
@CucumberOptions(
glue = ["net.corda.behave.scenarios"],
plugin = ["pretty"]
)
@Suppress("KDocMissingDocumentation")
class CucumberTest

View File

@ -1,60 +0,0 @@
@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,85 +0,0 @@
package net.corda.behave.scenarios
import cucumber.api.java.After
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 {
private val nodes = mutableListOf<Node.Builder>()
private var network: Network? = null
fun fail(message: String) {
error<Unit>(message)
}
fun <T> error(message: String, ex: Throwable? = null): T {
this.network?.signalFailure(message, ex)
if (ex != null) {
throw Exception(message, ex)
} else {
throw Exception(message)
}
}
fun node(name: String): Node {
val network = network ?: error("Network is not running")
return network[nodeName(name)] ?: error("Node '$name' not found")
}
fun nodeBuilder(name: String): Node.Builder {
return nodes.firstOrNull { it.name == nodeName(name) } ?: newNode(name)
}
fun ensureNetworkIsRunning(timeout: Duration? = null) {
if (network != null) {
// Network is already running
return
}
val networkBuilder = Network.new()
for (node in nodes) {
networkBuilder.addNode(node)
}
network = networkBuilder.generate()
network?.start()
assertThat(network?.waitUntilRunning(timeout)).isTrue()
}
inline fun <T> withNetwork(action: ScenarioState.() -> T): T {
ensureNetworkIsRunning()
return action()
}
inline fun <T> withClient(nodeName: String, crossinline action: (CordaRPCOps) -> T): T {
withNetwork {
return node(nodeName).rpc {
action(it)
}
}
}
@After
fun stopNetwork() {
val network = network ?: return
for (node in network) {
val matches = node.logOutput.find("\\[ERR")
if (matches.any()) {
fail("Found errors in the log for node '${node.config.name}': ${matches.first().filename}")
}
}
network.stop()
}
private fun nodeName(name: String) = name
private fun newNode(name: String): Node.Builder {
val builder = Node.new()
.withName(nodeName(name))
nodes.add(builder)
return builder
}
}

View File

@ -1,53 +0,0 @@
package net.corda.behave.scenarios
import cucumber.api.java8.En
import io.github.classgraph.ClassGraph
import net.corda.behave.scenarios.api.StepsBlock
import net.corda.behave.scenarios.api.StepsProvider
import net.corda.behave.scenarios.steps.*
import net.corda.core.internal.objectOrNewInstance
import net.corda.core.internal.pooledScan
import net.corda.core.utilities.contextLogger
@Suppress("KDocMissingDocumentation")
class StepsContainer(val state: ScenarioState) : En {
companion object {
private val log = contextLogger()
val stepsProviders: List<StepsProvider> by lazy {
ClassGraph()
.addClassLoader(this::class.java.classLoader)
.enableAllInfo()
.pooledScan()
.use { it.getClassesImplementing(StepsProvider::class.java.name).loadClasses(StepsProvider::class.java) }
.map { it.kotlin.objectOrNewInstance() }
}
}
private val stepDefinitions: List<StepsBlock> = listOf(
CashSteps(),
ConfigurationSteps(),
DatabaseSteps(),
NetworkSteps(),
RpcSteps(),
SshSteps(),
StartupSteps(),
VaultSteps()
)
init {
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 steps(action: (StepsContainer.() -> Unit)) {
action(this)
}
}

View File

@ -1,16 +0,0 @@
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

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

View File

@ -1,65 +0,0 @@
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.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.finance.internal.CashConfigDataFlow
import java.util.*
import java.util.concurrent.TimeUnit
class Cash(state: ScenarioState) : Substeps(state) {
fun numberOfIssuableCurrencies(nodeName: String): Int {
return withClient(nodeName) {
try {
val config = it.startFlow(::CashConfigDataFlow).returnValue.get(10, TimeUnit.SECONDS)
for (supportedCurrency in config.supportedCurrencies) {
log.info("Can use $supportedCurrency")
}
for (issuableCurrency in config.issuableCurrencies) {
log.info("Can issue $issuableCurrency")
}
return@withClient config.issuableCurrencies.size
} catch (ex: Exception) {
log.warn("Failed to retrieve cash configuration data", ex)
throw ex
}
}
}
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,22 +0,0 @@
package net.corda.behave.scenarios.helpers
import net.corda.behave.await
import net.corda.behave.scenarios.ScenarioState
import net.corda.core.utilities.seconds
import org.assertj.core.api.Assertions.assertThat
import java.util.concurrent.CountDownLatch
class Database(state: ScenarioState) : Substeps(state) {
fun canConnectTo(nodeName: String) {
withNetwork {
val latch = CountDownLatch(1)
log.info("Connecting to the database of node '$nodeName' ...")
node(nodeName).database.use {
log.info("Connected to the database of node '$nodeName'")
latch.countDown()
}
assertThat(latch.await(10.seconds)).isTrue()
}
}
}

View File

@ -1,42 +0,0 @@
package net.corda.behave.scenarios.helpers
import net.corda.behave.scenarios.ScenarioState
import org.assertj.core.api.Assertions.assertThat
import rx.observers.TestSubscriber
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class Ssh(state: ScenarioState) : Substeps(state) {
fun canConnectTo(nodeName: String) {
withNetwork {
log.info("Connecting to node '$nodeName' over SSH ...")
hasSshStartupMessage(nodeName)
val latch = CountDownLatch(1)
val subscriber = TestSubscriber<String>()
node(nodeName).ssh {
it.output.subscribe(subscriber)
assertThat(subscriber.onNextEvents).isNotEmpty
log.info("Successfully connect to node '$nodeName' over SSH")
latch.countDown()
}
if (!latch.await(15, TimeUnit.SECONDS)) {
fail("Failed to connect to node '$nodeName' over SSH")
}
}
}
private fun hasSshStartupMessage(nodeName: String) {
var i = 5
while (i > 0) {
Thread.sleep(2000)
if (state.node(nodeName).logOutput.find(".*SSH server listening on port.*").any()) {
break
}
i -= 1
}
if (i == 0) {
state.fail("Unable to find SSH start-up message for node $nodeName")
}
}
}

View File

@ -1,113 +0,0 @@
package net.corda.behave.scenarios.helpers
import net.corda.behave.process.JarCommand
import net.corda.behave.scenarios.ScenarioState
import net.corda.core.internal.div
import net.corda.core.utilities.minutes
class Startup(state: ScenarioState) : Substeps(state) {
fun hasLoggingInformation(nodeName: String) {
withNetwork {
log.info("Retrieving logging information for node '$nodeName' ...")
if (!node(nodeName).logOutput.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)
}
}
}
}
fun hasDatabaseDetails(nodeName: String) {
withNetwork {
log.info("Retrieving database details for node '$nodeName' ...")
if (!node(nodeName).logOutput.find("Database connection url is.*").any()) {
fail("Unable to find database details for node $nodeName")
}
}
}
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' ...")
val logOutput = node(nodeName).logOutput
if (!logOutput.find(".*Platform Version: $platformVersion .*").any()) {
val match = logOutput.find(".*Platform Version: .*").firstOrNull()
if (match == null) {
fail("Unable to find platform version for node '$nodeName'")
} else {
val foundVersion = Regex("Platform Version: (\\d+) ")
.find(match.contents)
?.groups?.last()?.value
fail("Expected platform version $platformVersion for node '$nodeName', " +
"but found version $foundVersion")
}
}
}
}
fun hasVersion(nodeName: String, version: String) {
withNetwork {
log.info("Finding version for node '$nodeName' ...")
val logOutput = node(nodeName).logOutput
if (!logOutput.find(".*Release: $version .*").any()) {
val match = logOutput.find(".*Release: .*").firstOrNull()
if (match == null) {
fail("Unable to find version for node '$nodeName'")
} else {
val foundVersion = Regex("Release: ([^ ]+) ")
.find(match.contents)
?.groups?.last()?.value
fail("Expected version $version for node '$nodeName', " +
"but found version $foundVersion")
}
}
}
}
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 = cordappDirectory / "$cordapp.jar"
// Execute
val command = JarCommand(cordappJar, args, cordappDirectory, 1.minutes)
command.start()
if (!command.waitFor())
fail("Failed to successfully run the CorDapp jar: $cordaApp")
}
}
}

View File

@ -1,22 +0,0 @@
package net.corda.behave.scenarios.helpers
import net.corda.behave.scenarios.ScenarioState
import net.corda.core.messaging.CordaRPCOps
import org.slf4j.Logger
import org.slf4j.LoggerFactory
abstract class Substeps(protected val state: ScenarioState) {
protected val log: Logger = LoggerFactory.getLogger(javaClass)
protected fun withNetwork(action: ScenarioState.() -> Unit) = state.withNetwork(action)
protected fun <T> withClient(nodeName: String, action: ScenarioState.(CordaRPCOps) -> T): T {
return state.withClient(nodeName) {
try {
action(state, it)
} catch (ex: Exception) {
state.error(ex.message ?: "Failed to execute RPC call")
}
}
}
}

View File

@ -1,21 +0,0 @@
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,37 +0,0 @@
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.Cash
import org.assertj.core.api.Assertions.assertThat
class CashSteps : StepsBlock {
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)
}
}
}
}

View File

@ -1,65 +0,0 @@
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.ScenarioState
import net.corda.behave.scenarios.api.StepsBlock
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)
}
}
}

View File

@ -1,18 +0,0 @@
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.Database
class DatabaseSteps : StepsBlock {
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,18 +0,0 @@
package net.corda.behave.scenarios.steps
import net.corda.behave.scenarios.ScenarioState
import net.corda.behave.scenarios.api.StepsBlock
import net.corda.core.utilities.minutes
class NetworkSteps : StepsBlock {
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,15 +0,0 @@
package net.corda.behave.scenarios.steps
import net.corda.behave.scenarios.ScenarioState
import net.corda.behave.scenarios.api.StepsBlock
class RpcSteps : StepsBlock {
override fun initialize(state: ScenarioState) {
Then<String>("^user can connect to node (\\w+) using RPC$") { name ->
state.withClient(name) {
succeed()
}
}
}
}

View File

@ -1,18 +0,0 @@
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.Ssh
class SshSteps : StepsBlock {
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,66 +0,0 @@
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.Startup
class StartupSteps : StepsBlock {
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)
}
}
}
}

View File

@ -1,45 +0,0 @@
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.internal.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 {
@Suppress("UNCHECKED_CAST")
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

@ -1,32 +0,0 @@
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

@ -1,35 +0,0 @@
@cash @issuance
Feature: Cash - Issuable Currencies
To have cash on ledger, certain nodes must have the ability to issue cash of various currencies.
Scenario Outline: Node can issue no currencies by default
Given a node PartyA of version <Node-Version>
And node PartyA has the finance app installed
When the network is ready
Then node PartyA has 0 issuable currencies
Examples:
| Node-Version |
| master |
Scenario Outline: Node has an issuable currency
Given a node PartyA of version <Node-Version>
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
Examples:
| Node-Version |
| master |
Scenario Outline: Node can issue a currency
Given a node PartyA of version <Node-Version>
And a nonvalidating notary Notary of version <Node-Version>
And node PartyA has the finance app installed
When the network is ready
Then node PartyA can issue 100 USD
Examples:
| Node-Version |
| master |

View File

@ -1,16 +0,0 @@
@database @connectivity
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 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 PartyA
Examples:
| Node-Version | Database-Type |
| master | H2 |
# To run this scenario using postgreSQL you must ensure that Docker is running locally
# | master | postgreSQL |

View File

@ -1,34 +0,0 @@
@logging @startup
Feature: Startup Information - Logging
A Corda node should inform the user of important parameters during startup so that he/she can confirm the setup and
configure / connect relevant software to said node.
Scenario Outline: Node shows logging information on startup
Given a node PartyA of version <Node-Version>
And node PartyA uses database of type <Database-Type>
And node PartyA is located in London, GB
When the network is ready
Then user can retrieve logging information for node PartyA
Examples:
| Node-Version | Database-Type |
| master | H2 |
Scenario Outline: Node shows database details on startup
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 retrieve database details for node PartyA
Examples:
| Node-Version | Database-Type |
| master | H2 |
Scenario Outline: Node shows version information on startup
Given a node PartyA of version <Node-Version>
Then node PartyA is on platform version <Platform-Version>
And node PartyA is on release version <Release-Version>
Examples:
| Node-Version | Platform-Version | Release-Version |
| master | 4 | corda-4.0-snapshot |

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info">
<ThresholdFilter level="info"/>
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT" ignoreExceptions="false">
<PatternLayout pattern="%m%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="STDOUT"/>
</Root>
</Loggers>
</Configuration>

View File

@ -1,26 +0,0 @@
#!/bin/bash
#
# Run this script from the experimental/behave directory
#
# $ pwd
# ./IdeaProjects/corda-reviews/experimental/behave
# $ src/scenario/resources/scripts/run-behave-features.sh
#
# Note: please ensure you have configured your staging environment by running the top-level script: prepare.sh
BUILD_DIR=$PWD
cd ${BUILD_DIR}
../../gradlew behaveJar
BEHAVE_JAR=$(ls build/libs/corda-behave-*.jar | tail -n1)
STAGING_ROOT=~/staging
# startup
java -DSTAGING_ROOT=${STAGING_ROOT} -jar ${BEHAVE_JAR} --glue net.corda.behave.scenarios -path ./src/scenario/resources/features/startup/logging.feature
# cash
java -DSTAGING_ROOT=${STAGING_ROOT} -jar ${BEHAVE_JAR} --glue net.corda.behave.scenarios -path ./src/scenario/resources/features/cash/currencies.feature
# database
java -DSTAGING_ROOT=${STAGING_ROOT} -jar ${BEHAVE_JAR} --glue net.corda.behave.scenarios -path ./src/scenario/resources/features/cash/currencies.feature

View File

@ -1,63 +0,0 @@
package net.corda.behave.monitoring
import net.corda.core.utilities.seconds
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import rx.Observable
class MonitoringTests {
@Test
fun `watch gets triggered when pattern is observed`() {
val observable = Observable.just("first", "second", "third")
val result = PatternWatch(observable, "c.n").await(1.seconds)
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(observable, "forth").await(1.seconds)
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(observable, "fir")
val watch2 = PatternWatch(observable, "ond")
val watch3 = PatternWatch(observable, "ird")
val aggregate = watch1 * watch2 * watch3
assertThat(aggregate.await(1.seconds)).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(observable, "fir")
val watch2 = PatternWatch(observable, "ond")
val watch3 = PatternWatch(observable, "baz")
val aggregate = watch1 * watch2 * watch3
assertThat(aggregate.await(1.seconds)).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(observable, "foo")
val watch2 = PatternWatch(observable, "ond")
val watch3 = PatternWatch(observable, "bar")
val aggregate = watch1 / watch2 / watch3
assertThat(aggregate.await(1.seconds)).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(observable, "foo")
val watch2 = PatternWatch(observable, "baz")
val watch3 = PatternWatch(observable, "bar")
val aggregate = watch1 / watch2 / watch3
assertThat(aggregate.await(1.seconds)).isFalse()
}
}

View File

@ -1,41 +0,0 @@
package net.corda.behave.network
import net.corda.behave.database.DatabaseType
import net.corda.behave.node.configuration.NotaryType
import net.corda.core.utilities.seconds
import org.junit.Ignore
import org.junit.Test
class NetworkTests {
@Ignore
@Test
fun `network of two nodes can be spun up`() {
val network = Network
.new()
.addNode("Foo")
.addNode("Bar")
.generate()
network.use {
it.waitUntilRunning(30.seconds)
it.signal()
it.keepAlive(30.seconds)
}
}
@Ignore
@Test
fun `network of three nodes and mixed databases can be spun up`() {
val network = Network
.new()
.addNode("Foo")
.addNode("Bar", databaseType = DatabaseType.POSTGRES)
.addNode("Baz", notaryType = NotaryType.NON_VALIDATING)
.generate()
network.use {
it.waitUntilRunning(30.seconds)
it.signal()
it.keepAlive(30.seconds)
}
}
}

View File

@ -1,31 +0,0 @@
package net.corda.behave.process
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import rx.observers.TestSubscriber
class CommandTests {
@Test
fun `successful command returns zero`() {
val exitCode = Command(listOf("ls", "/")).run()
assertThat(exitCode).isEqualTo(0)
}
@Test
fun `failed command returns non-zero`() {
val exitCode = Command(listOf("ls", "some-weird-path-that-does-not-exist")).run()
assertThat(exitCode).isNotEqualTo(0)
}
@Test
fun `output stream for command can be observed`() {
val subscriber = TestSubscriber<String>()
val exitCode = Command(listOf("ls", "/")).run(subscriber) { _, _ ->
subscriber.awaitTerminalEvent()
subscriber.assertCompleted()
subscriber.assertNoErrors()
assertThat(subscriber.onNextEvents).contains("bin", "etc", "var")
}
assertThat(exitCode).isEqualTo(0)
}
}

View File

@ -1,18 +0,0 @@
package net.corda.behave.service
import net.corda.behave.service.database.PostgreSQLService
import org.assertj.core.api.Assertions.assertThat
import org.junit.Ignore
import org.junit.Test
class PostreSQLServiceTests {
@Ignore
@Test
fun `postgres can be started and stopped`() {
val service = PostgreSQLService("test-postgres", 12345, "postgres")
val didStart = service.start()
service.stop()
assertThat(didStart).isTrue()
}
}

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info">
<ThresholdFilter level="info"/>
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT" ignoreExceptions="false">
<PatternLayout pattern="%m%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="STDOUT"/>
</Root>
</Loggers>
</Configuration>

View File

@ -28,7 +28,6 @@ include 'webserver'
include 'webserver:webcapsule'
include 'experimental'
include 'experimental:avalanche'
include 'experimental:behave'
include 'experimental:quasar-hook'
include 'experimental:corda-utils'
include 'experimental:nodeinfo'