mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Port library functionality from corda/behave
This commit is contained in:
parent
f3d2a7674c
commit
cd079de4b8
11
experimental/behave/README.md
Normal file
11
experimental/behave/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Setup
|
||||
|
||||
To get started, please run the following command:
|
||||
|
||||
```bash
|
||||
$ ./prepare.sh
|
||||
```
|
||||
|
||||
This command will download necessary database drivers and set up
|
||||
the dependencies directory with copies of the Corda fat-JAR and
|
||||
the network bootstrapping tool.
|
@ -1,6 +1,6 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.2.21'
|
||||
ext.commonsio_version = '2.6'
|
||||
ext.commonslogging_version = '1.2'
|
||||
ext.cucumber_version = '1.2.5'
|
||||
ext.crash_version = 'cce5a00f114343c1145c1d7756e1dd6df3ea984e'
|
||||
ext.docker_client_version = '8.11.0'
|
||||
@ -48,7 +48,7 @@ dependencies {
|
||||
|
||||
// Library
|
||||
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
|
||||
compile("com.github.corda.crash:crash.shell:$crash_version") {
|
||||
@ -61,23 +61,30 @@ dependencies {
|
||||
exclude group: "org.bouncycastle"
|
||||
}
|
||||
|
||||
compile "org.slf4j:log4j-over-slf4j:$slf4j_version"
|
||||
compile "org.slf4j:jul-to-slf4j:$slf4j_version"
|
||||
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||
compile "org.apache.logging.log4j:log4j-core:$log4j_version"
|
||||
|
||||
compile "commons-io:commons-io:$commonsio_version"
|
||||
compile "commons-logging:commons-logging:$commonslogging_version"
|
||||
compile "com.spotify:docker-client:$docker_client_version"
|
||||
compile "io.reactivex:rxjava:$rxjava_version"
|
||||
|
||||
compile project(':finance')
|
||||
compile project(':node-api')
|
||||
compile project(':client:rpc')
|
||||
|
||||
// Unit Tests
|
||||
|
||||
testCompile "junit:junit:$junit_version"
|
||||
testCompile "org.assertj:assertj-core:$assertj_version"
|
||||
|
||||
// Scenarios / End-to-End Tests
|
||||
|
||||
scenarioCompile "info.cukes:cucumber-java8:$cucumber_version"
|
||||
scenarioCompile "info.cukes:cucumber-junit:$cucumber_version"
|
||||
scenarioCompile "info.cukes:cucumber-picocontainer:$cucumber_version"
|
||||
scenarioCompile "org.assertj:assertj-core:$assertj_version"
|
||||
scenarioCompile "org.slf4j:log4j-over-slf4j:$slf4j_version"
|
||||
scenarioCompile "org.slf4j:jul-to-slf4j:$slf4j_version"
|
||||
scenarioCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||
scenarioCompile "org.apache.logging.log4j:log4j-core:$log4j_version"
|
||||
scenarioCompile "commons-io:commons-io:$commonsio_version"
|
||||
|
||||
}
|
||||
|
||||
@ -103,5 +110,5 @@ task scenarios(type: Test) {
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
scenarios.mustRunAfter test
|
||||
scenarios.dependsOn test
|
||||
//scenarios.mustRunAfter test
|
||||
//scenarios.dependsOn test
|
1
experimental/behave/deps/.gitignore
vendored
Normal file
1
experimental/behave/deps/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.jar
|
0
experimental/behave/deps/corda/3.0.0/.gitkeep
Normal file
0
experimental/behave/deps/corda/3.0.0/.gitkeep
Normal file
0
experimental/behave/deps/corda/3.0.0/apps/.gitkeep
Normal file
0
experimental/behave/deps/corda/3.0.0/apps/.gitkeep
Normal file
0
experimental/behave/deps/drivers/.gitkeep
Normal file
0
experimental/behave/deps/drivers/.gitkeep
Normal file
3
experimental/behave/deps/drivers/README.md
Normal file
3
experimental/behave/deps/drivers/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
Download and store database drivers here; for example:
|
||||
- h2-1.4.196.jar
|
||||
- mssql-jdbc-6.2.2.jre8.jar
|
24
experimental/behave/prepare.sh
Executable file
24
experimental/behave/prepare.sh
Executable file
@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
VERSION=3.0.0
|
||||
|
||||
# Set up directories
|
||||
mkdir -p deps/corda/${VERSION}/apps
|
||||
mkdir -p deps/drivers
|
||||
|
||||
# Copy Corda capsule into deps
|
||||
cp ../../node/capsule/build/libs/corda-*.jar deps/corda/${VERSION}/corda.jar
|
||||
|
||||
# Download database drivers
|
||||
curl "https://search.maven.org/remotecontent?filepath=com/h2database/h2/1.4.196/h2-1.4.196.jar" > deps/drivers/h2-1.4.196.jar
|
||||
curl -L "https://github.com/Microsoft/mssql-jdbc/releases/download/v6.2.2/mssql-jdbc-6.2.2.jre8.jar" > deps/drivers/mssql-jdbc-6.2.2.jre8.jar
|
||||
|
||||
# Build required artefacts
|
||||
cd ../..
|
||||
./gradlew buildBootstrapperJar
|
||||
./gradlew :finance:jar
|
||||
|
||||
# Copy build artefacts into deps
|
||||
cd experimental/behave
|
||||
cp ../../tools/bootstrapper/build/libs/*.jar deps/corda/${VERSION}/network-bootstrapper.jar
|
||||
cp ../../finance/build/libs/corda-finance-*.jar deps/corda/${VERSION}/apps/corda-finance.jar
|
@ -0,0 +1,28 @@
|
||||
package net.corda.behave
|
||||
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
val Int.millisecond: Duration
|
||||
get() = Duration.ofMillis(this.toLong())
|
||||
|
||||
val Int.milliseconds: Duration
|
||||
get() = Duration.ofMillis(this.toLong())
|
||||
|
||||
val Int.second: Duration
|
||||
get() = Duration.ofSeconds(this.toLong())
|
||||
|
||||
val Int.seconds: Duration
|
||||
get() = Duration.ofSeconds(this.toLong())
|
||||
|
||||
val Int.minute: Duration
|
||||
get() = Duration.ofMinutes(this.toLong())
|
||||
|
||||
val Int.minutes: Duration
|
||||
get() = Duration.ofMinutes(this.toLong())
|
||||
|
||||
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,7 +0,0 @@
|
||||
package net.corda.behave
|
||||
|
||||
object Utility {
|
||||
|
||||
fun dummy() = true
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
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()
|
||||
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
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.put("user", config.username)
|
||||
connectionProps.put("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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
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
|
||||
|
||||
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 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
|
||||
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package net.corda.behave.database
|
||||
|
||||
import net.corda.behave.database.configuration.H2ConfigurationTemplate
|
||||
import net.corda.behave.database.configuration.SqlServerConfigurationTemplate
|
||||
import net.corda.behave.node.configuration.Configuration
|
||||
import net.corda.behave.node.configuration.DatabaseConfiguration
|
||||
import net.corda.behave.service.database.H2Service
|
||||
import net.corda.behave.service.database.SqlServerService
|
||||
|
||||
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)
|
||||
}
|
||||
),
|
||||
|
||||
SQL_SERVER(DatabaseSettings()
|
||||
.withDatabase(SqlServerService.database)
|
||||
.withSchema(SqlServerService.schema)
|
||||
.withUser(SqlServerService.username)
|
||||
.withConfigTemplate(SqlServerConfigurationTemplate())
|
||||
.withServiceInitiator {
|
||||
SqlServerService("sqlserver-${it.name}", it.database.port, it.database.password)
|
||||
}
|
||||
);
|
||||
|
||||
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
|
||||
"sql_server" -> SQL_SERVER
|
||||
"sql server" -> SQL_SERVER
|
||||
"sqlserver" -> SQL_SERVER
|
||||
else -> null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
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}
|
||||
"""
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package net.corda.behave.database.configuration
|
||||
|
||||
import net.corda.behave.database.DatabaseConfigurationTemplate
|
||||
import net.corda.behave.node.configuration.DatabaseConfiguration
|
||||
|
||||
class SqlServerConfigurationTemplate : DatabaseConfigurationTemplate() {
|
||||
|
||||
override val connectionString: (DatabaseConfiguration) -> String
|
||||
get() = { "jdbc:sqlserver://${it.host}:${it.port};database=${it.database}" }
|
||||
|
||||
override val config: (DatabaseConfiguration) -> String
|
||||
get() = {
|
||||
"""
|
||||
|dataSourceProperties = {
|
||||
| dataSourceClassName = "com.microsoft.sqlserver.jdbc.SQLServerDataSource"
|
||||
| dataSource.url = "${connectionString(it)}"
|
||||
| dataSource.user = "${it.username}"
|
||||
| dataSource.password = "${it.password}"
|
||||
|}
|
||||
|database = {
|
||||
| initialiseSchema=true
|
||||
| transactionIsolationLevel = READ_COMMITTED
|
||||
| schema="${it.schema}"
|
||||
|}
|
||||
"""
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package net.corda.behave.file
|
||||
|
||||
import java.io.File
|
||||
|
||||
val currentDirectory: File
|
||||
get() = File(System.getProperty("user.dir"))
|
||||
|
||||
operator fun File.div(relative: String): File = this.resolve(relative)
|
@ -0,0 +1,42 @@
|
||||
package net.corda.behave.file
|
||||
|
||||
import java.io.File
|
||||
|
||||
class LogSource(
|
||||
private val directory: File,
|
||||
filePattern: String? = ".*\\.log",
|
||||
private val filePatternUsedForExclusion: Boolean = false
|
||||
) {
|
||||
|
||||
private val fileRegex = Regex(filePattern ?: ".*")
|
||||
|
||||
data class MatchedLogContent(
|
||||
val filename: File,
|
||||
val contents: String
|
||||
)
|
||||
|
||||
fun find(pattern: String? = null): List<MatchedLogContent> {
|
||||
val regex = if (pattern != null) {
|
||||
Regex(pattern)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val logFiles = directory.listFiles({ file ->
|
||||
(!filePatternUsedForExclusion && file.name.matches(fileRegex)) ||
|
||||
(filePatternUsedForExclusion && !file.name.matches(fileRegex))
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package net.corda.behave.logging
|
||||
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
inline fun <reified T> getLogger(): Logger =
|
||||
LoggerFactory.getLogger(T::class.java)
|
@ -0,0 +1,23 @@
|
||||
package net.corda.behave.monitoring
|
||||
|
||||
import net.corda.behave.await
|
||||
import rx.Observable
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
class ConjunctiveWatch(
|
||||
private val left: Watch,
|
||||
private val right: Watch
|
||||
) : Watch() {
|
||||
|
||||
override fun await(observable: Observable<String>, timeout: Duration): Boolean {
|
||||
val latch = CountDownLatch(2)
|
||||
listOf(left, right).parallelStream().forEach {
|
||||
if (it.await(observable, timeout)) {
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
return latch.await(timeout)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package net.corda.behave.monitoring
|
||||
|
||||
import net.corda.behave.await
|
||||
import rx.Observable
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
class DisjunctiveWatch(
|
||||
private val left: Watch,
|
||||
private val right: Watch
|
||||
) : Watch() {
|
||||
|
||||
override fun await(observable: Observable<String>, timeout: Duration): Boolean {
|
||||
val latch = CountDownLatch(1)
|
||||
listOf(left, right).parallelStream().forEach {
|
||||
if (it.await(observable, timeout)) {
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
return latch.await(timeout)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
package net.corda.behave.monitoring
|
||||
|
||||
class PatternWatch(
|
||||
pattern: String,
|
||||
ignoreCase: Boolean = false
|
||||
) : Watch() {
|
||||
|
||||
private val regularExpression = if (ignoreCase) {
|
||||
Regex("^.*$pattern.*$", RegexOption.IGNORE_CASE)
|
||||
} else {
|
||||
Regex("^.*$pattern.*$")
|
||||
}
|
||||
|
||||
override fun match(data: String) = regularExpression.matches(data.trim())
|
||||
|
||||
companion object {
|
||||
|
||||
val EMPTY = PatternWatch("")
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package net.corda.behave.monitoring
|
||||
|
||||
import net.corda.behave.await
|
||||
import net.corda.behave.seconds
|
||||
import rx.Observable
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
abstract class Watch {
|
||||
|
||||
private val latch = CountDownLatch(1)
|
||||
|
||||
open fun await(
|
||||
observable: Observable<String>,
|
||||
timeout: Duration = 10.seconds
|
||||
): Boolean {
|
||||
observable
|
||||
.filter { match(it) }
|
||||
.forEach { latch.countDown() }
|
||||
return latch.await(timeout)
|
||||
}
|
||||
|
||||
open fun match(data: String): Boolean = false
|
||||
|
||||
operator fun times(other: Watch): Watch {
|
||||
return ConjunctiveWatch(this, other)
|
||||
}
|
||||
|
||||
operator fun div(other: Watch): Watch {
|
||||
return DisjunctiveWatch(this, other)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,301 @@
|
||||
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.div
|
||||
import net.corda.behave.logging.getLogger
|
||||
import net.corda.behave.minutes
|
||||
import net.corda.behave.node.Distribution
|
||||
import net.corda.behave.node.Node
|
||||
import net.corda.behave.node.configuration.NotaryType
|
||||
import net.corda.behave.process.JarCommand
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
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: File,
|
||||
private val timeout: Duration = 2.minutes
|
||||
) : Closeable, Iterable<Node> {
|
||||
|
||||
private val log = getLogger<Network>()
|
||||
|
||||
private val latch = CountDownLatch(1)
|
||||
|
||||
private var isRunning = false
|
||||
|
||||
private var isStopped = false
|
||||
|
||||
private var hasError = false
|
||||
|
||||
class Builder internal constructor(
|
||||
private val timeout: Duration
|
||||
) {
|
||||
|
||||
private val nodes = mutableMapOf<String, Node>()
|
||||
|
||||
private val startTime = DateTimeFormatter
|
||||
.ofPattern("yyyyMMDD-HHmmss")
|
||||
.withZone(ZoneId.of("UTC"))
|
||||
.format(Instant.now())
|
||||
|
||||
private val directory = currentDirectory / "build/runs/$startTime"
|
||||
|
||||
fun addNode(
|
||||
name: String,
|
||||
distribution: Distribution = Distribution.LATEST_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.bootstrapNetwork()
|
||||
return network
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun copyDatabaseDrivers() {
|
||||
val driverDirectory = targetDirectory / "libs"
|
||||
FileUtils.forceMkdir(driverDirectory)
|
||||
FileUtils.copyDirectory(
|
||||
currentDirectory / "deps/drivers",
|
||||
driverDirectory
|
||||
)
|
||||
}
|
||||
|
||||
private 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 bootstrapNetwork() {
|
||||
copyDatabaseDrivers()
|
||||
if (!configureNodes()) {
|
||||
hasError = true
|
||||
return
|
||||
}
|
||||
val bootstrapper = nodes.values
|
||||
.sortedByDescending { it.config.distribution.version }
|
||||
.first()
|
||||
.config.distribution.networkBootstrapper
|
||||
|
||||
if (!bootstrapper.exists()) {
|
||||
log.warn("Network bootstrapping tool does not exist; continuing ...")
|
||||
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.absolutePath }
|
||||
for (match in matches) {
|
||||
log.info("Log(${match.key}):\n${match.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 ...")
|
||||
FileUtils.deleteDirectory(targetDirectory)
|
||||
} else {
|
||||
log.info("Deleting temporary files, but retaining logs and config ...")
|
||||
for (node in nodes.values.map { it.config.name }) {
|
||||
val nodeFolder = targetDirectory / node
|
||||
FileUtils.deleteDirectory(nodeFolder / "additional-node-infos")
|
||||
FileUtils.deleteDirectory(nodeFolder / "artemis")
|
||||
FileUtils.deleteDirectory(nodeFolder / "certificates")
|
||||
FileUtils.deleteDirectory(nodeFolder / "cordapps")
|
||||
FileUtils.deleteDirectory(nodeFolder / "shell-commands")
|
||||
FileUtils.deleteDirectory(nodeFolder / "sshkey")
|
||||
FileUtils.deleteQuietly(nodeFolder / "corda.jar")
|
||||
FileUtils.deleteQuietly(nodeFolder / "network-parameters")
|
||||
FileUtils.deleteQuietly(nodeFolder / "persistence.mv.db")
|
||||
FileUtils.deleteQuietly(nodeFolder / "process-id")
|
||||
|
||||
for (nodeInfo in nodeFolder.listFiles({
|
||||
file -> file.name.matches(Regex("nodeInfo-.*"))
|
||||
})) {
|
||||
FileUtils.deleteQuietly(nodeInfo)
|
||||
}
|
||||
}
|
||||
FileUtils.deleteDirectory(targetDirectory / "libs")
|
||||
FileUtils.deleteDirectory(targetDirectory / ".cache")
|
||||
}
|
||||
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) {
|
||||
node.start()
|
||||
}
|
||||
}
|
||||
|
||||
fun waitUntilRunning(waitDuration: Duration? = null): Boolean {
|
||||
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
|
||||
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 {
|
||||
|
||||
const val CLEANUP_ON_ERROR = false
|
||||
|
||||
fun new(
|
||||
timeout: Duration = 2.minutes
|
||||
): Builder = Builder(timeout)
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package net.corda.behave.node
|
||||
|
||||
import net.corda.behave.file.div
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
|
||||
/**
|
||||
* 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: File? = null,
|
||||
|
||||
/**
|
||||
* The URL of the distribution fat JAR, if available.
|
||||
*/
|
||||
val url: URL? = null
|
||||
|
||||
) {
|
||||
|
||||
/**
|
||||
* The path to the distribution fat JAR.
|
||||
*/
|
||||
val jarFile: File = file ?: nodePrefix / "$version/corda.jar"
|
||||
|
||||
/**
|
||||
* The path to available Cordapps for this distribution.
|
||||
*/
|
||||
val cordappDirectory: File = nodePrefix / "$version/apps"
|
||||
|
||||
/**
|
||||
* The path to network bootstrapping tool.
|
||||
*/
|
||||
val networkBootstrapper: File = nodePrefix / "$version/network-bootstrapper.jar"
|
||||
|
||||
/**
|
||||
* Ensure that the distribution is available on disk.
|
||||
*/
|
||||
fun ensureAvailable() {
|
||||
if (!jarFile.exists()) {
|
||||
if (url != null) {
|
||||
try {
|
||||
FileUtils.forceMkdirParent(jarFile)
|
||||
FileUtils.copyURLToFile(url, jarFile)
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Invalid Corda version $version", e)
|
||||
}
|
||||
} else {
|
||||
throw Exception("File not found $jarFile")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable representation of the distribution.
|
||||
*/
|
||||
override fun toString() = "Corda(version = $version, path = $jarFile)"
|
||||
|
||||
companion object {
|
||||
|
||||
private val distributions = mutableListOf<Distribution>()
|
||||
|
||||
private val directory = File(System.getProperty("user.dir"))
|
||||
|
||||
private val nodePrefix = directory / "deps/corda"
|
||||
|
||||
/**
|
||||
* Corda Open Source, version 3.0.0
|
||||
*/
|
||||
val V3 = fromJarFile("3.0.0")
|
||||
|
||||
val LATEST_MASTER = V3
|
||||
|
||||
/**
|
||||
* Get representation of an open source distribution based on its version string.
|
||||
* @param version The version of the open source Corda distribution.
|
||||
*/
|
||||
fun fromOpenSourceVersion(version: String): Distribution {
|
||||
val url = URL("https://dl.bintray.com/r3/corda/net/corda/corda/$version/corda-$version.jar")
|
||||
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: File? = null): Distribution {
|
||||
val distribution = Distribution(version, file = jarFile)
|
||||
distributions.add(distribution)
|
||||
return distribution
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registered representation of a Corda distribution based on its version string.
|
||||
* @param version The version of the Corda distribution
|
||||
*/
|
||||
fun fromVersionString(version: String): Distribution? = when (version.toLowerCase()) {
|
||||
"master" -> LATEST_MASTER
|
||||
else -> distributions.firstOrNull { it.version == version }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,321 @@
|
||||
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.div
|
||||
import net.corda.behave.logging.getLogger
|
||||
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 org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
/**
|
||||
* Corda node.
|
||||
*/
|
||||
class Node(
|
||||
val config: Configuration,
|
||||
private val rootDirectory: File = currentDirectory,
|
||||
private val settings: ServiceSettings = ServiceSettings()
|
||||
) {
|
||||
|
||||
private val log = getLogger<Node>()
|
||||
|
||||
private val runtimeDirectory = rootDirectory / config.name
|
||||
|
||||
private val logDirectory = runtimeDirectory / "logs"
|
||||
|
||||
private val command = JarCommand(
|
||||
config.distribution.jarFile,
|
||||
arrayOf("--config", "node.conf"),
|
||||
runtimeDirectory,
|
||||
settings.timeout,
|
||||
enableRemoteDebugging = false
|
||||
)
|
||||
|
||||
private val isAliveLatch = PatternWatch("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}.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(command.output, 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 {
|
||||
LogSource(logDirectory, "node-info-gen.log", filePatternUsedForExclusion = true)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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 = rootDirectory / "../../../deps/corda/$version/apps"
|
||||
if (appDirectory.exists()) {
|
||||
val targetAppDirectory = runtimeDirectory / "cordapps"
|
||||
FileUtils.copyDirectory(appDirectory, targetAppDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
class Builder {
|
||||
|
||||
var name: String? = null
|
||||
private set
|
||||
|
||||
private var distribution = Distribution.V3
|
||||
|
||||
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: File? = 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: File): 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,
|
||||
configElements = *arrayOf(
|
||||
NotaryConfiguration(notaryType),
|
||||
CurrencyConfiguration(issuableCurrencies),
|
||||
CordappConfiguration(
|
||||
apps = *apps.toTypedArray(),
|
||||
includeFinance = includeFinance
|
||||
)
|
||||
)
|
||||
),
|
||||
directory,
|
||||
ServiceSettings(timeout)
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T> error(message: String): T {
|
||||
throw IllegalArgumentException(message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun new() = Builder()
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package net.corda.behave.node.configuration
|
||||
|
||||
import net.corda.behave.database.DatabaseType
|
||||
import net.corda.behave.node.*
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
|
||||
class Configuration(
|
||||
val name: String,
|
||||
val distribution: Distribution = Distribution.LATEST_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
|
||||
),
|
||||
vararg configElements: ConfigurationTemplate
|
||||
) {
|
||||
|
||||
private val developerMode = true
|
||||
|
||||
private val useHttps = false
|
||||
|
||||
private val basicConfig = """
|
||||
|myLegalName="C=$country,L=$location,O=$name"
|
||||
|keyStorePassword="cordacadevpass"
|
||||
|trustStorePassword="trustpass"
|
||||
|extraAdvertisedServiceIds=[ "" ]
|
||||
|useHTTPS=$useHttps
|
||||
|devMode=$developerMode
|
||||
|jarDirs = [ "../libs" ]
|
||||
""".trimMargin()
|
||||
|
||||
private val extraConfig = (configElements.toList() + listOf(users, nodeInterface))
|
||||
.joinToString(separator = "\n") { it.generate(this) }
|
||||
|
||||
fun writeToFile(file: File) {
|
||||
FileUtils.writeStringToFile(file, this.generate(), "UTF-8")
|
||||
}
|
||||
|
||||
private fun generate() = listOf(basicConfig, database.config(), extraConfig)
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString("\n")
|
||||
|
||||
companion object {
|
||||
|
||||
private val DEFAULT_PASSWORD = "S0meS3cretW0rd"
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package net.corda.behave.node.configuration
|
||||
|
||||
open class ConfigurationTemplate {
|
||||
|
||||
protected open val config: (Configuration) -> String = { "" }
|
||||
|
||||
fun generate(config: Configuration) = config(config).trimMargin()
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package net.corda.behave.node.configuration
|
||||
|
||||
class CordappConfiguration(vararg apps: String, var includeFinance: Boolean = false) : ConfigurationTemplate() {
|
||||
|
||||
private val applications = apps.toList() + 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)}\""
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package net.corda.behave.node.configuration
|
||||
|
||||
class CurrencyConfiguration(private val issuableCurrencies: List<String>) : ConfigurationTemplate() {
|
||||
|
||||
override val config: (Configuration) -> String
|
||||
get() = {
|
||||
if (issuableCurrencies.isEmpty()) {
|
||||
""
|
||||
} else {
|
||||
"""
|
||||
|issuableCurrencies=[
|
||||
| ${issuableCurrencies.joinToString(", ")}
|
||||
|]
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
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)
|
||||
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
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))
|
||||
) : 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"
|
||||
|}
|
||||
|webAddress="$host:$webPort"
|
||||
"""
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package net.corda.behave.node.configuration
|
||||
|
||||
class NotaryConfiguration(private val notaryType: NotaryType) : ConfigurationTemplate() {
|
||||
|
||||
override val config: (Configuration) -> String
|
||||
get() = {
|
||||
when (notaryType) {
|
||||
NotaryType.NONE -> ""
|
||||
NotaryType.NON_VALIDATING ->
|
||||
"notary { validating = false }"
|
||||
NotaryType.VALIDATING ->
|
||||
"notary { validating = true }"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
package net.corda.behave.process
|
||||
|
||||
import net.corda.behave.*
|
||||
import net.corda.behave.file.currentDirectory
|
||||
import net.corda.behave.logging.getLogger
|
||||
import net.corda.behave.process.output.OutputListener
|
||||
import rx.Observable
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
open class Command(
|
||||
private val command: List<String>,
|
||||
private val directory: File = currentDirectory,
|
||||
private val timeout: Duration = 2.minutes
|
||||
): Closeable {
|
||||
|
||||
protected val log = getLogger<Command>()
|
||||
|
||||
private val terminationLatch = CountDownLatch(1)
|
||||
|
||||
private val outputCapturedLatch = CountDownLatch(1)
|
||||
|
||||
private var isInterrupted = false
|
||||
|
||||
private var process: Process? = null
|
||||
|
||||
private lateinit var outputListener: OutputListener
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val thread = Thread(Runnable {
|
||||
try {
|
||||
val processBuilder = ProcessBuilder(command)
|
||||
.directory(directory)
|
||||
.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
|
||||
outputListener.onNewLine(line)
|
||||
} catch (_: IOException) {
|
||||
break
|
||||
}
|
||||
}
|
||||
input.close()
|
||||
outputListener.onEndOfStream()
|
||||
outputCapturedLatch.countDown()
|
||||
}).start()
|
||||
val streamIsClosed = outputCapturedLatch.await(timeout)
|
||||
val timeout = if (!streamIsClosed || isInterrupted) {
|
||||
1.second
|
||||
} 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)
|
||||
}
|
||||
process = null
|
||||
terminationLatch.countDown()
|
||||
})
|
||||
|
||||
fun start() {
|
||||
output.subscribe()
|
||||
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() {
|
||||
waitFor()
|
||||
}
|
||||
|
||||
fun run() = use { _ -> }
|
||||
|
||||
fun use(action: (Command) -> Unit): Int {
|
||||
try {
|
||||
start()
|
||||
action(this)
|
||||
} finally {
|
||||
close()
|
||||
}
|
||||
return exitCode
|
||||
}
|
||||
|
||||
fun use(action: (Command, Observable<String>) -> Unit = { _, _ -> }): Int {
|
||||
try {
|
||||
start()
|
||||
action(this, output)
|
||||
} finally {
|
||||
close()
|
||||
}
|
||||
return exitCode
|
||||
}
|
||||
|
||||
override fun toString() = "Command(${command.joinToString(" ")})"
|
||||
|
||||
companion object {
|
||||
|
||||
private val WAIT_BEFORE_KILL: Duration = 5.seconds
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package net.corda.behave.process
|
||||
|
||||
import java.io.File
|
||||
import java.time.Duration
|
||||
|
||||
class JarCommand(
|
||||
jarFile: File,
|
||||
arguments: Array<String>,
|
||||
directory: File,
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package net.corda.behave.process.output
|
||||
|
||||
interface OutputListener {
|
||||
|
||||
fun onNewLine(line: String)
|
||||
|
||||
fun onEndOfStream()
|
||||
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
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 net.corda.behave.monitoring.Watch
|
||||
import rx.Observable
|
||||
import java.io.Closeable
|
||||
|
||||
abstract class ContainerService(
|
||||
name: String,
|
||||
port: Int,
|
||||
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 var startupStatement: Watch = PatternWatch.EMPTY
|
||||
|
||||
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()
|
||||
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")
|
||||
}
|
||||
|
||||
protected fun setStartupStatement(statement: String) {
|
||||
startupStatement = PatternWatch(statement)
|
||||
}
|
||||
|
||||
override fun checkPrerequisites() {
|
||||
if (!client.listImages().any { true == it.repoTags()?.contains(imageReference) }) {
|
||||
log.info("Pulling image $imageReference ...")
|
||||
client.pull(imageReference, { _ ->
|
||||
run { }
|
||||
})
|
||||
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"))
|
||||
if (startupStatement.await(observable, 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package net.corda.behave.service
|
||||
|
||||
import net.corda.behave.logging.getLogger
|
||||
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 = getLogger<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
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package net.corda.behave.service
|
||||
|
||||
import net.corda.behave.node.configuration.Configuration
|
||||
|
||||
typealias ServiceInitiator = (Configuration) -> Service
|
@ -0,0 +1,13 @@
|
||||
package net.corda.behave.service
|
||||
|
||||
import net.corda.behave.minute
|
||||
import net.corda.behave.second
|
||||
import net.corda.behave.seconds
|
||||
import java.time.Duration
|
||||
|
||||
data class ServiceSettings(
|
||||
val timeout: Duration = 1.minute,
|
||||
val startupDelay: Duration = 1.second,
|
||||
val startupTimeout: Duration = 15.seconds,
|
||||
val pollInterval: Duration = 1.second
|
||||
)
|
@ -0,0 +1,18 @@
|
||||
package net.corda.behave.service.database
|
||||
|
||||
import net.corda.behave.service.Service
|
||||
|
||||
class H2Service(
|
||||
name: String,
|
||||
port: Int
|
||||
) : Service(name, port) {
|
||||
|
||||
companion object {
|
||||
|
||||
val database = "node"
|
||||
val schema = "dbo"
|
||||
val username = "sa"
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package net.corda.behave.service.database
|
||||
|
||||
import net.corda.behave.database.DatabaseConnection
|
||||
import net.corda.behave.database.DatabaseType
|
||||
import net.corda.behave.database.configuration.SqlServerConfigurationTemplate
|
||||
import net.corda.behave.node.configuration.DatabaseConfiguration
|
||||
import net.corda.behave.service.ContainerService
|
||||
import net.corda.behave.service.ServiceSettings
|
||||
|
||||
class SqlServerService(
|
||||
name: String,
|
||||
port: Int,
|
||||
private val password: String,
|
||||
settings: ServiceSettings = ServiceSettings()
|
||||
) : ContainerService(name, port, settings) {
|
||||
|
||||
override val baseImage = "microsoft/mssql-server-linux"
|
||||
|
||||
override val internalPort = 1433
|
||||
|
||||
init {
|
||||
addEnvironmentVariable("ACCEPT_EULA", "Y")
|
||||
addEnvironmentVariable("SA_PASSWORD", password)
|
||||
setStartupStatement("SQL Server is now ready for client connections")
|
||||
}
|
||||
|
||||
override fun verify(): Boolean {
|
||||
val config = DatabaseConfiguration(
|
||||
type = DatabaseType.SQL_SERVER,
|
||||
host = host,
|
||||
port = port,
|
||||
database = database,
|
||||
schema = schema,
|
||||
username = username,
|
||||
password = password
|
||||
)
|
||||
val connection = DatabaseConnection(config, SqlServerConfigurationTemplate())
|
||||
try {
|
||||
connection.use {
|
||||
return true
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
log.warn(ex.message, ex)
|
||||
ex.printStackTrace()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val host = "localhost"
|
||||
val database = "master"
|
||||
val schema = "dbo"
|
||||
val username = "sa"
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
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) {
|
||||
try {
|
||||
action(this)
|
||||
} finally {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
fun write(vararg bytes: Byte) = client.write(*bytes)
|
||||
|
||||
fun write(charSequence: CharSequence) = client.write(charSequence)
|
||||
|
||||
fun writeLine(string: String) = client.writeLine(string)
|
||||
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
package net.corda.behave.ssh
|
||||
|
||||
import net.corda.behave.logging.getLogger
|
||||
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 = getLogger<SSHClient>()
|
||||
|
||||
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")
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -12,6 +12,7 @@ class ScenarioHooks(private val state: ScenarioState) {
|
||||
|
||||
@After
|
||||
fun afterScenario() {
|
||||
state.stopNetwork()
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,102 @@
|
||||
package net.corda.behave.scenarios
|
||||
|
||||
import net.corda.behave.logging.getLogger
|
||||
import net.corda.behave.network.Network
|
||||
import net.corda.behave.node.Node
|
||||
import net.corda.behave.seconds
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.client.rpc.CordaRPCClientConfiguration
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
|
||||
class ScenarioState {
|
||||
|
||||
var count: Int = 0
|
||||
private val log = getLogger<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() {
|
||||
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()).isTrue()
|
||||
}
|
||||
|
||||
fun withNetwork(action: ScenarioState.() -> Unit) {
|
||||
ensureNetworkIsRunning()
|
||||
action()
|
||||
}
|
||||
|
||||
fun <T> withClient(nodeName: String, action: (CordaRPCOps) -> T): T {
|
||||
var result: T? = null
|
||||
withNetwork {
|
||||
val node = node(nodeName)
|
||||
val user = node.config.users.first()
|
||||
val address = node.config.nodeInterface
|
||||
val targetHost = NetworkHostAndPort(address.host, address.rpcPort)
|
||||
val config = CordaRPCClientConfiguration(
|
||||
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")
|
||||
}
|
||||
|
||||
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) = "Entity$name"
|
||||
|
||||
private fun newNode(name: String): Node.Builder {
|
||||
val builder = Node.new()
|
||||
.withName(nodeName(name))
|
||||
nodes.add(builder)
|
||||
return builder
|
||||
}
|
||||
|
||||
}
|
@ -1,23 +1,58 @@
|
||||
package net.corda.behave.scenarios
|
||||
|
||||
import cucumber.api.java8.En
|
||||
import net.corda.behave.scenarios.steps.dummySteps
|
||||
import net.corda.behave.scenarios.helpers.Cash
|
||||
import net.corda.behave.scenarios.helpers.Database
|
||||
import net.corda.behave.scenarios.helpers.Ssh
|
||||
import net.corda.behave.scenarios.helpers.Startup
|
||||
import net.corda.behave.scenarios.steps.*
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
@Suppress("KDocMissingDocumentation")
|
||||
class StepsContainer(val state: ScenarioState) : En {
|
||||
|
||||
val log: Logger = LoggerFactory.getLogger(StepsContainer::class.java)
|
||||
private val log: Logger = LoggerFactory.getLogger(StepsContainer::class.java)
|
||||
|
||||
private val stepDefinitions: List<(StepsBlock) -> Unit> = listOf(
|
||||
::dummySteps
|
||||
::cashSteps,
|
||||
::configurationSteps,
|
||||
::databaseSteps,
|
||||
::networkSteps,
|
||||
::rpcSteps,
|
||||
::sshSteps,
|
||||
::startupSteps
|
||||
)
|
||||
|
||||
init {
|
||||
stepDefinitions.forEach { it({ this.steps(it) }) }
|
||||
}
|
||||
|
||||
fun succeed() = log.info("Step succeeded")
|
||||
|
||||
fun fail(message: String) = state.fail(message)
|
||||
|
||||
fun<T> error(message: String) = state.error<T>(message)
|
||||
|
||||
fun node(name: String) = state.nodeBuilder(name)
|
||||
|
||||
fun withNetwork(action: ScenarioState.() -> Unit) {
|
||||
state.withNetwork(action)
|
||||
}
|
||||
|
||||
fun <T> withClient(nodeName: String, action: (CordaRPCOps) -> T): T {
|
||||
return state.withClient(nodeName, action)
|
||||
}
|
||||
|
||||
val startup = Startup(state)
|
||||
|
||||
val database = Database(state)
|
||||
|
||||
val ssh = Ssh(state)
|
||||
|
||||
val cash = Cash(state)
|
||||
|
||||
private fun steps(action: (StepsContainer.() -> Unit)) {
|
||||
action(this)
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
package net.corda.behave.scenarios.helpers
|
||||
|
||||
import net.corda.behave.scenarios.ScenarioState
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.finance.flows.CashConfigDataFlow
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class Cash(state: ScenarioState) : Substeps(state) {
|
||||
|
||||
fun numberOfIssuableCurrencies(nodeName: String): Int {
|
||||
return withClient(nodeName) {
|
||||
for (flow in it.registeredFlows()) {
|
||||
log.info(flow)
|
||||
}
|
||||
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 (_: Exception) {
|
||||
return@withClient 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package net.corda.behave.scenarios.helpers
|
||||
|
||||
import net.corda.behave.await
|
||||
import net.corda.behave.scenarios.ScenarioState
|
||||
import net.corda.behave.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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package net.corda.behave.scenarios.helpers
|
||||
|
||||
import net.corda.behave.scenarios.ScenarioState
|
||||
|
||||
class Startup(state: ScenarioState) : Substeps(state) {
|
||||
|
||||
fun hasLoggingInformation(nodeName: String) {
|
||||
withNetwork {
|
||||
log.info("Retrieving logging information for node '$nodeName' ...")
|
||||
if (!node(nodeName).nodeInfoGenerationOutput.find("Logs can be found in.*").any()) {
|
||||
fail("Unable to find logging information for node $nodeName")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun hasDatabaseDetails(nodeName: String) {
|
||||
withNetwork {
|
||||
log.info("Retrieving database details for node '$nodeName' ...")
|
||||
if (!node(nodeName).nodeInfoGenerationOutput.find("Database connection url is.*").any()) {
|
||||
fail("Unable to find database details for node $nodeName")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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("Version: ([^ ]+) ")
|
||||
.find(match.contents)
|
||||
?.groups?.last()?.value
|
||||
fail("Expected version $version for node '$nodeName', " +
|
||||
"but found version $foundVersion")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package net.corda.behave.scenarios.helpers
|
||||
|
||||
import net.corda.behave.logging.getLogger
|
||||
import net.corda.behave.scenarios.ScenarioState
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
|
||||
abstract class Substeps(protected val state: ScenarioState) {
|
||||
|
||||
protected val log = getLogger<Substeps>()
|
||||
|
||||
protected fun withNetwork(action: ScenarioState.() -> Unit) =
|
||||
state.withNetwork(action)
|
||||
|
||||
protected fun <T> withClient(nodeName: String, action: ScenarioState.(CordaRPCOps) -> T): T {
|
||||
return state.withClient(nodeName, {
|
||||
return@withClient try {
|
||||
action(state, it)
|
||||
} catch (ex: Exception) {
|
||||
state.error<T>(ex.message ?: "Failed to execute RPC call")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package net.corda.behave.scenarios.steps
|
||||
|
||||
import net.corda.behave.scenarios.StepsBlock
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
|
||||
fun cashSteps(steps: StepsBlock) = steps {
|
||||
|
||||
Then<String>("^node (\\w+) has 1 issuable currency$") { name ->
|
||||
withNetwork {
|
||||
assertThat(cash.numberOfIssuableCurrencies(name)).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
||||
Then<String, String>("^node (\\w+) has (\\w+) issuable currencies$") { name, count ->
|
||||
withNetwork {
|
||||
assertThat(cash.numberOfIssuableCurrencies(name)).isEqualTo(count.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package net.corda.behave.scenarios.steps
|
||||
|
||||
import net.corda.behave.database.DatabaseType
|
||||
import net.corda.behave.node.Distribution
|
||||
import net.corda.behave.node.configuration.toNotaryType
|
||||
import net.corda.behave.scenarios.StepsBlock
|
||||
|
||||
fun configurationSteps(steps: StepsBlock) = steps {
|
||||
|
||||
Given<String, String>("^a node (\\w+) of version ([^ ]+)$") { name, version ->
|
||||
node(name)
|
||||
.withDistribution(Distribution.fromVersionString(version)
|
||||
?: error("Unknown version '$version'"))
|
||||
}
|
||||
|
||||
Given<String, String, String>("^a (\\w+) notary (\\w+) of version ([^ ]+)$") { type, name, version ->
|
||||
node(name)
|
||||
.withDistribution(Distribution.fromVersionString(version)
|
||||
?: error("Unknown version '$version'"))
|
||||
.withNotaryType(type.toNotaryType()
|
||||
?: error("Unknown notary type '$type'"))
|
||||
}
|
||||
|
||||
Given<String, String>("^node (\\w+) uses database of type (.+)$") { name, type ->
|
||||
node(name)
|
||||
.withDatabaseType(DatabaseType.fromName(type)
|
||||
?: error("Unknown database type '$type'"))
|
||||
}
|
||||
|
||||
Given<String, String>("^node (\\w+) can issue (.+)$") { name, currencies ->
|
||||
node(name).withIssuableCurrencies(currencies
|
||||
.replace(" and ", ", ")
|
||||
.split(", ")
|
||||
.map { it.toUpperCase() })
|
||||
}
|
||||
|
||||
Given<String, String, String>("^node (\\w+) is located in (\\w+), (\\w+)$") { name, location, country ->
|
||||
node(name).withLocation(location, country)
|
||||
}
|
||||
|
||||
Given<String>("^node (\\w+) has the finance app installed$") { name ->
|
||||
node(name).withFinanceApp()
|
||||
}
|
||||
|
||||
Given<String, String>("^node (\\w+) has app installed: (.+)$") { name, app ->
|
||||
node(name).withApp(app)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package net.corda.behave.scenarios.steps
|
||||
|
||||
import net.corda.behave.scenarios.StepsBlock
|
||||
|
||||
fun databaseSteps(steps: StepsBlock) = steps {
|
||||
|
||||
Then<String>("^user can connect to the database of node (\\w+)$") { name ->
|
||||
withNetwork {
|
||||
database.canConnectTo(name)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package net.corda.behave.scenarios.steps
|
||||
|
||||
import net.corda.behave.scenarios.StepsBlock
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
|
||||
fun dummySteps(steps: StepsBlock) = steps {
|
||||
|
||||
When<Int, String>("^(\\d+) dumm(y|ies) exists?$") { count, _ ->
|
||||
state.count = count
|
||||
log.info("Checking pre-condition $count")
|
||||
}
|
||||
|
||||
Then("^there is a dummy$") {
|
||||
assertThat(state.count).isGreaterThan(0)
|
||||
log.info("Checking outcome ${state.count}")
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package net.corda.behave.scenarios.steps
|
||||
|
||||
import net.corda.behave.scenarios.StepsBlock
|
||||
|
||||
fun networkSteps(steps: StepsBlock) = steps {
|
||||
|
||||
When("^the network is ready$") {
|
||||
state.ensureNetworkIsRunning()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package net.corda.behave.scenarios.steps
|
||||
|
||||
import net.corda.behave.scenarios.StepsBlock
|
||||
|
||||
fun rpcSteps(steps: StepsBlock) = steps {
|
||||
|
||||
Then<String>("^user can connect to node (\\w+) using RPC$") { name ->
|
||||
withClient(name) {
|
||||
succeed()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package net.corda.behave.scenarios.steps
|
||||
|
||||
import net.corda.behave.scenarios.StepsBlock
|
||||
|
||||
fun sshSteps(steps: StepsBlock) = steps {
|
||||
|
||||
Then<String>("^user can connect to node (\\w+) using SSH$") { name ->
|
||||
withNetwork {
|
||||
ssh.canConnectTo(name)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package net.corda.behave.scenarios.steps
|
||||
|
||||
import net.corda.behave.scenarios.StepsBlock
|
||||
|
||||
fun startupSteps(steps: StepsBlock) = steps {
|
||||
|
||||
Then<String>("^user can retrieve database details for node (\\w+)$") { name ->
|
||||
withNetwork {
|
||||
startup.hasDatabaseDetails(name)
|
||||
}
|
||||
}
|
||||
|
||||
Then<String>("^user can retrieve logging information for node (\\w+)$") { name ->
|
||||
withNetwork {
|
||||
startup.hasLoggingInformation(name)
|
||||
}
|
||||
}
|
||||
|
||||
Then<String, String>("^node (\\w+) is on version ([^ ]+)$") { name, version ->
|
||||
withNetwork {
|
||||
startup.hasVersion(name, version)
|
||||
}
|
||||
}
|
||||
|
||||
Then<String, String>("^node (\\w+) is on platform version (\\w+)$") { name, platformVersion ->
|
||||
withNetwork {
|
||||
startup.hasPlatformVersion(name, platformVersion.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
Feature: Cash - Issuable Currencies
|
||||
To have cash on ledger, certain nodes must have the ability to issue cash of various currencies.
|
||||
|
||||
Scenario: Node can issue no currencies by default
|
||||
Given a node A of version master
|
||||
And node A has the finance app installed
|
||||
When the network is ready
|
||||
Then node A has 0 issuable currencies
|
||||
|
||||
Scenario: Node can issue a currency
|
||||
Given a node A of version master
|
||||
And node A can issue USD
|
||||
When the network is ready
|
||||
Then node A has 1 issuable currency
|
@ -0,0 +1,13 @@
|
||||
Feature: Database - Connection
|
||||
For Corda to work, a database must be running and appropriately configured.
|
||||
|
||||
Scenario Outline: User can connect to node's database
|
||||
Given a node A of version <Node-Version>
|
||||
And node A uses database of type <Database-Type>
|
||||
When the network is ready
|
||||
Then user can connect to the database of node A
|
||||
|
||||
Examples:
|
||||
| Node-Version | Database-Type |
|
||||
| MASTER | H2 |
|
||||
#| MASTER | SQL Server |
|
@ -1,6 +0,0 @@
|
||||
Feature: Dummy
|
||||
Lorem ipsum
|
||||
|
||||
Scenario: Noop
|
||||
Given 15 dummies exist
|
||||
Then there is a dummy
|
@ -0,0 +1,13 @@
|
||||
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: Node shows logging information on startup
|
||||
Given a node A of version MASTER
|
||||
And node A uses database of type H2
|
||||
And node A is located in London, GB
|
||||
When the network is ready
|
||||
Then node A is on platform version 2
|
||||
And node A is on version 3.0-SNAPSHOT
|
||||
And user can retrieve logging information for node A
|
||||
And user can retrieve database details for node A
|
@ -1,13 +0,0 @@
|
||||
package net.corda.behave
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class UtilityTests {
|
||||
|
||||
@Test
|
||||
fun `dummy`() {
|
||||
Assert.assertEquals(true, Utility.dummy())
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package net.corda.behave.monitoring
|
||||
|
||||
import net.corda.behave.second
|
||||
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("c.n").await(observable, 1.second)
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `watch does not get triggered when pattern is not observed`() {
|
||||
val observable = Observable.just("first", "second", "third")
|
||||
val result = PatternWatch("forth").await(observable, 1.second)
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `conjunctive watch gets triggered when all its constituents match on the input`() {
|
||||
val observable = Observable.just("first", "second", "third")
|
||||
val watch1 = PatternWatch("fir")
|
||||
val watch2 = PatternWatch("ond")
|
||||
val watch3 = PatternWatch("ird")
|
||||
val aggregate = watch1 * watch2 * watch3
|
||||
assertThat(aggregate.await(observable, 1.second)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `conjunctive watch does not get triggered when one or more of its constituents do not match on the input`() {
|
||||
val observable = Observable.just("first", "second", "third")
|
||||
val watch1 = PatternWatch("fir")
|
||||
val watch2 = PatternWatch("ond")
|
||||
val watch3 = PatternWatch("baz")
|
||||
val aggregate = watch1 * watch2 * watch3
|
||||
assertThat(aggregate.await(observable, 1.second)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disjunctive watch gets triggered when one or more of its constituents match on the input`() {
|
||||
val observable = Observable.just("first", "second", "third")
|
||||
val watch1 = PatternWatch("foo")
|
||||
val watch2 = PatternWatch("ond")
|
||||
val watch3 = PatternWatch("bar")
|
||||
val aggregate = watch1 / watch2 / watch3
|
||||
assertThat(aggregate.await(observable, 1.second)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disjunctive watch does not get triggered when none its constituents match on the input`() {
|
||||
val observable = Observable.just("first", "second", "third")
|
||||
val watch1 = PatternWatch("foo")
|
||||
val watch2 = PatternWatch("baz")
|
||||
val watch3 = PatternWatch("bar")
|
||||
val aggregate = watch1 / watch2 / watch3
|
||||
assertThat(aggregate.await(observable, 1.second)).isFalse()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package net.corda.behave.network
|
||||
|
||||
import net.corda.behave.database.DatabaseType
|
||||
import net.corda.behave.node.configuration.NotaryType
|
||||
import net.corda.behave.seconds
|
||||
import org.junit.Test
|
||||
|
||||
class NetworkTests {
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `network of three nodes and mixed databases can be spun up`() {
|
||||
val network = Network
|
||||
.new()
|
||||
.addNode("Foo")
|
||||
.addNode("Bar", databaseType = DatabaseType.SQL_SERVER)
|
||||
.addNode("Baz", notaryType = NotaryType.NON_VALIDATING)
|
||||
.generate()
|
||||
network.use {
|
||||
it.waitUntilRunning(30.seconds)
|
||||
it.signal()
|
||||
it.keepAlive(30.seconds)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package net.corda.behave.process
|
||||
|
||||
import org.assertj.core.api.Assertions.*
|
||||
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("some-random-command-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", "/")).use { _, output ->
|
||||
output.subscribe(subscriber)
|
||||
subscriber.awaitTerminalEvent()
|
||||
subscriber.assertCompleted()
|
||||
subscriber.assertNoErrors()
|
||||
assertThat(subscriber.onNextEvents).contains("bin", "etc", "var")
|
||||
}
|
||||
assertThat(exitCode).isEqualTo(0)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package net.corda.behave.service
|
||||
|
||||
import net.corda.behave.service.database.SqlServerService
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class SqlServerServiceTests {
|
||||
|
||||
@Test
|
||||
fun `sql server can be started and stopped`() {
|
||||
val service = SqlServerService("test-mssql", 12345, "S0meS3cretW0rd")
|
||||
val didStart = service.start()
|
||||
service.stop()
|
||||
assertThat(didStart).isTrue()
|
||||
}
|
||||
|
||||
}
|
14
experimental/behave/src/test/resources/log4j2.xml
Normal file
14
experimental/behave/src/test/resources/log4j2.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?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>
|
Loading…
Reference in New Issue
Block a user