Port library functionality from corda/behave

This commit is contained in:
Tommy Lillehagen 2018-02-09 12:58:38 +00:00
parent f3d2a7674c
commit cd079de4b8
72 changed files with 2892 additions and 58 deletions

View 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.

View File

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

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

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

View File

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

View File

@ -1,7 +0,0 @@
package net.corda.behave
object Utility {
fun dummy() = true
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ class ScenarioHooks(private val state: ScenarioState) {
@After
fun afterScenario() {
state.stopNetwork()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +0,0 @@
Feature: Dummy
Lorem ipsum
Scenario: Noop
Given 15 dummies exist
Then there is a dummy

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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