mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
Removed experimental/behave (#5212)
The behave framework is in its own repo. This code is no longer used.
This commit is contained in:
parent
d72ec57c1c
commit
c32e3a9e26
@ -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"
|
|
||||||
```
|
|
@ -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'
|
|
||||||
}
|
|
||||||
}
|
|
1
experimental/behave/deps/.gitignore
vendored
1
experimental/behave/deps/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
*.jar
|
|
@ -1,4 +0,0 @@
|
|||||||
Download and store database drivers here; for example:
|
|
||||||
- h2-1.4.196.jar
|
|
||||||
- postgresql-42.1.4.jar
|
|
||||||
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
|}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
@ -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)}\""
|
|
||||||
}
|
|
||||||
}
|
|
@ -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(", ")}
|
|
||||||
| ]
|
|
||||||
|}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 }"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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(" ")})"
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package net.corda.behave.process.output
|
|
||||||
|
|
||||||
interface OutputListener {
|
|
||||||
|
|
||||||
fun onNewLine(line: String)
|
|
||||||
|
|
||||||
fun onEndOfStream()
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
package net.corda.behave.service
|
|
||||||
|
|
||||||
import net.corda.behave.node.configuration.Configuration
|
|
||||||
|
|
||||||
typealias ServiceInitiator = (Configuration) -> Service
|
|
@ -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
|
|
||||||
)
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
|
@ -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)
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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")
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package net.corda.behave.scenarios.api
|
|
||||||
|
|
||||||
interface StepsProvider {
|
|
||||||
val name: String
|
|
||||||
val stepsDefinition: StepsBlock
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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>
|
|
@ -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
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
|
@ -28,7 +28,6 @@ include 'webserver'
|
|||||||
include 'webserver:webcapsule'
|
include 'webserver:webcapsule'
|
||||||
include 'experimental'
|
include 'experimental'
|
||||||
include 'experimental:avalanche'
|
include 'experimental:avalanche'
|
||||||
include 'experimental:behave'
|
|
||||||
include 'experimental:quasar-hook'
|
include 'experimental:quasar-hook'
|
||||||
include 'experimental:corda-utils'
|
include 'experimental:corda-utils'
|
||||||
include 'experimental:nodeinfo'
|
include 'experimental:nodeinfo'
|
||||||
|
Loading…
Reference in New Issue
Block a user