From f3d2a7674cc22476d162347d76cef3225afb3026 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Thu, 1 Feb 2018 16:06:12 +0000 Subject: [PATCH 1/8] Add module for end-to-end testing library --- .idea/compiler.xml | 5 +- experimental/behave/build.gradle | 107 ++++++++++++++++++ .../main/kotlin/net/corda/behave/Utility.kt | 7 ++ .../behave/src/scenario/kotlin/Scenarios.kt | 12 ++ .../corda/behave/scenarios/ScenarioHooks.kt | 17 +++ .../corda/behave/scenarios/ScenarioState.kt | 7 ++ .../net/corda/behave/scenarios/StepsBlock.kt | 3 + .../corda/behave/scenarios/StepsContainer.kt | 25 ++++ .../behave/scenarios/steps/DummySteps.kt | 18 +++ .../scenario/resources/features/dummy.feature | 6 + .../behave/src/scenario/resources/log4j2.xml | 14 +++ .../kotlin/net/corda/behave/UtilityTests.kt | 13 +++ settings.gradle | 1 + 13 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 experimental/behave/build.gradle create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt create mode 100644 experimental/behave/src/scenario/kotlin/Scenarios.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsBlock.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt create mode 100644 experimental/behave/src/scenario/resources/features/dummy.feature create mode 100644 experimental/behave/src/scenario/resources/log4j2.xml create mode 100644 experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt diff --git a/.idea/compiler.xml b/.idea/compiler.xml index a33ce1e0f08..4f9145d5f4f 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -10,6 +10,9 @@ + + + @@ -159,4 +162,4 @@ - \ No newline at end of file + diff --git a/experimental/behave/build.gradle b/experimental/behave/build.gradle new file mode 100644 index 00000000000..1c420488ce0 --- /dev/null +++ b/experimental/behave/build.gradle @@ -0,0 +1,107 @@ +buildscript { + ext.kotlin_version = '1.2.21' + ext.commonsio_version = '2.6' + ext.cucumber_version = '1.2.5' + ext.crash_version = 'cce5a00f114343c1145c1d7756e1dd6df3ea984e' + ext.docker_client_version = '8.11.0' + + repositories { + maven { + jcenter() + url 'https://jitpack.io' + } + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +group 'net.corda.behave' + +apply plugin: 'java' +apply plugin: 'kotlin' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +sourceSets { + scenario { + java { + compileClasspath += main.output + runtimeClasspath += main.output + srcDir file('src/scenario/kotlin') + } + resources.srcDir file('src/scenario/resources') + } +} + +configurations { + scenarioCompile.extendsFrom testCompile + scenarioRuntime.extendsFrom testRuntime +} + +dependencies { + + // Library + + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + + compile("com.github.corda.crash:crash.shell:$crash_version") { + exclude group: "org.slf4j", module: "slf4j-jdk14" + exclude group: "org.bouncycastle" + } + + compile("com.github.corda.crash:crash.connectors.ssh:$crash_version") { + exclude group: "org.slf4j", module: "slf4j-jdk14" + exclude group: "org.bouncycastle" + } + + compile "com.spotify:docker-client:$docker_client_version" + + // Unit Tests + + testCompile "junit:junit:$junit_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" + +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +compileScenarioKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +test { + testLogging.showStandardStreams = true +} + +task scenarios(type: Test) { + setTestClassesDirs sourceSets.scenario.output.getClassesDirs() + classpath = sourceSets.scenario.runtimeClasspath + outputs.upToDateWhen { false } +} + +scenarios.mustRunAfter test +scenarios.dependsOn test \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt b/experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt new file mode 100644 index 00000000000..b3dd28f73c9 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt @@ -0,0 +1,7 @@ +package net.corda.behave + +object Utility { + + fun dummy() = true + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/Scenarios.kt b/experimental/behave/src/scenario/kotlin/Scenarios.kt new file mode 100644 index 00000000000..b0c96a98ee8 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/Scenarios.kt @@ -0,0 +1,12 @@ +import cucumber.api.CucumberOptions +import cucumber.api.junit.Cucumber +import org.junit.runner.RunWith + +@RunWith(Cucumber::class) +@CucumberOptions( + features = arrayOf("src/scenario/resources/features"), + glue = arrayOf("net.corda.behave.scenarios"), + plugin = arrayOf("pretty") +) +@Suppress("KDocMissingDocumentation") +class CucumberTest diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt new file mode 100644 index 00000000000..745ef851b67 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt @@ -0,0 +1,17 @@ +package net.corda.behave.scenarios + +import cucumber.api.java.After +import cucumber.api.java.Before + +@Suppress("KDocMissingDocumentation") +class ScenarioHooks(private val state: ScenarioState) { + + @Before + fun beforeScenario() { + } + + @After + fun afterScenario() { + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt new file mode 100644 index 00000000000..a21ab59a108 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt @@ -0,0 +1,7 @@ +package net.corda.behave.scenarios + +class ScenarioState { + + var count: Int = 0 + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsBlock.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsBlock.kt new file mode 100644 index 00000000000..5880c939a37 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsBlock.kt @@ -0,0 +1,3 @@ +package net.corda.behave.scenarios + +typealias StepsBlock = (StepsContainer.() -> Unit) -> Unit \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt new file mode 100644 index 00000000000..69ef2938538 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt @@ -0,0 +1,25 @@ +package net.corda.behave.scenarios + +import cucumber.api.java8.En +import net.corda.behave.scenarios.steps.dummySteps +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 stepDefinitions: List<(StepsBlock) -> Unit> = listOf( + ::dummySteps + ) + + init { + stepDefinitions.forEach { it({ this.steps(it) }) } + } + + private fun steps(action: (StepsContainer.() -> Unit)) { + action(this) + } + +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt new file mode 100644 index 00000000000..ce86fa51866 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt @@ -0,0 +1,18 @@ +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("^(\\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}") + } + +} diff --git a/experimental/behave/src/scenario/resources/features/dummy.feature b/experimental/behave/src/scenario/resources/features/dummy.feature new file mode 100644 index 00000000000..6ff1613bd55 --- /dev/null +++ b/experimental/behave/src/scenario/resources/features/dummy.feature @@ -0,0 +1,6 @@ +Feature: Dummy + Lorem ipsum + + Scenario: Noop + Given 15 dummies exist + Then there is a dummy \ No newline at end of file diff --git a/experimental/behave/src/scenario/resources/log4j2.xml b/experimental/behave/src/scenario/resources/log4j2.xml new file mode 100644 index 00000000000..43fcf63c3d5 --- /dev/null +++ b/experimental/behave/src/scenario/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt new file mode 100644 index 00000000000..c956cc67e1f --- /dev/null +++ b/experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt @@ -0,0 +1,13 @@ +package net.corda.behave + +import org.junit.Assert +import org.junit.Test + +class UtilityTests { + + @Test + fun `dummy`() { + Assert.assertEquals(true, Utility.dummy()) + } + +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index a4f87dee28c..4b0a92c815e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,7 @@ include 'client:rpc' include 'webserver' include 'webserver:webcapsule' include 'experimental' +include 'experimental:behave' include 'experimental:sandbox' include 'experimental:quasar-hook' include 'experimental:kryo-hook' From cd079de4b8aed5831c942d8b51b7b98b3fcfcce7 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 9 Feb 2018 12:58:38 +0000 Subject: [PATCH 2/8] Port library functionality from corda/behave --- experimental/behave/README.md | 11 + experimental/behave/build.gradle | 27 +- experimental/behave/deps/.gitignore | 1 + experimental/behave/deps/corda/3.0.0/.gitkeep | 0 .../behave/deps/corda/3.0.0/apps/.gitkeep | 0 experimental/behave/deps/drivers/.gitkeep | 0 experimental/behave/deps/drivers/README.md | 3 + experimental/behave/prepare.sh | 24 ++ .../main/kotlin/net/corda/behave/Utilities.kt | 28 ++ .../main/kotlin/net/corda/behave/Utility.kt | 7 - .../database/DatabaseConfigurationTemplate.kt | 13 + .../behave/database/DatabaseConnection.kt | 85 +++++ .../corda/behave/database/DatabaseSettings.kt | 59 ++++ .../net/corda/behave/database/DatabaseType.kt | 48 +++ .../configuration/H2ConfigurationTemplate.kt | 18 + .../SqlServerConfigurationTemplate.kt | 28 ++ .../net/corda/behave/file/FileUtilities.kt | 8 + .../kotlin/net/corda/behave/file/LogSource.kt | 42 +++ .../net/corda/behave/logging/LogUtilities.kt | 7 + .../behave/monitoring/ConjunctiveWatch.kt | 23 ++ .../behave/monitoring/DisjunctiveWatch.kt | 24 ++ .../corda/behave/monitoring/PatternWatch.kt | 22 ++ .../net/corda/behave/monitoring/Watch.kt | 33 ++ .../net/corda/behave/network/Network.kt | 301 ++++++++++++++++ .../net/corda/behave/node/Distribution.kt | 116 +++++++ .../main/kotlin/net/corda/behave/node/Node.kt | 321 ++++++++++++++++++ .../node/configuration/Configuration.kt | 56 +++ .../configuration/ConfigurationTemplate.kt | 9 + .../configuration/CordappConfiguration.kt | 28 ++ .../configuration/CurrencyConfiguration.kt | 18 + .../configuration/DatabaseConfiguration.kt | 17 + .../node/configuration/NetworkInterface.kt | 65 ++++ .../node/configuration/NotaryConfiguration.kt | 16 + .../behave/node/configuration/NotaryType.kt | 18 + .../node/configuration/UserConfiguration.kt | 37 ++ .../net/corda/behave/process/Command.kt | 157 +++++++++ .../net/corda/behave/process/JarCommand.kt | 34 ++ .../behave/process/output/OutputListener.kt | 9 + .../corda/behave/service/ContainerService.kt | 122 +++++++ .../net/corda/behave/service/Service.kt | 72 ++++ .../corda/behave/service/ServiceInitiator.kt | 5 + .../corda/behave/service/ServiceSettings.kt | 13 + .../behave/service/database/H2Service.kt | 18 + .../service/database/SqlServerService.kt | 58 ++++ .../corda/behave/ssh/MonitoringSSHClient.kt | 69 ++++ .../kotlin/net/corda/behave/ssh/SSHClient.kt | 161 +++++++++ .../corda/behave/scenarios/ScenarioHooks.kt | 1 + .../corda/behave/scenarios/ScenarioState.kt | 97 +++++- .../corda/behave/scenarios/StepsContainer.kt | 41 ++- .../corda/behave/scenarios/helpers/Cash.kt | 30 ++ .../behave/scenarios/helpers/Database.kt | 23 ++ .../net/corda/behave/scenarios/helpers/Ssh.kt | 43 +++ .../corda/behave/scenarios/helpers/Startup.kt | 65 ++++ .../behave/scenarios/helpers/Substeps.kt | 24 ++ .../corda/behave/scenarios/steps/CashSteps.kt | 20 ++ .../scenarios/steps/ConfigurationSteps.kt | 49 +++ .../behave/scenarios/steps/DatabaseSteps.kt | 13 + .../behave/scenarios/steps/DummySteps.kt | 18 - .../behave/scenarios/steps/NetworkSteps.kt | 11 + .../corda/behave/scenarios/steps/RpcSteps.kt | 13 + .../corda/behave/scenarios/steps/SshSteps.kt | 13 + .../behave/scenarios/steps/StartupSteps.kt | 31 ++ .../features/cash/currencies.feature | 14 + .../features/database/connection.feature | 13 + .../scenario/resources/features/dummy.feature | 6 - .../features/startup/logging.feature | 13 + .../kotlin/net/corda/behave/UtilityTests.kt | 13 - .../behave/monitoring/MonitoringTests.kt | 64 ++++ .../net/corda/behave/network/NetworkTests.kt | 39 +++ .../net/corda/behave/process/CommandTests.kt | 34 ++ .../behave/service/SqlServerServiceTests.kt | 17 + .../behave/src/test/resources/log4j2.xml | 14 + 72 files changed, 2892 insertions(+), 58 deletions(-) create mode 100644 experimental/behave/README.md create mode 100644 experimental/behave/deps/.gitignore create mode 100644 experimental/behave/deps/corda/3.0.0/.gitkeep create mode 100644 experimental/behave/deps/corda/3.0.0/apps/.gitkeep create mode 100644 experimental/behave/deps/drivers/.gitkeep create mode 100644 experimental/behave/deps/drivers/README.md create mode 100755 experimental/behave/prepare.sh create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/Utilities.kt delete mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConfigurationTemplate.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConnection.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseSettings.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseType.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/H2ConfigurationTemplate.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/SqlServerConfigurationTemplate.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/file/FileUtilities.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/file/LogSource.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/logging/LogUtilities.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/monitoring/ConjunctiveWatch.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/monitoring/DisjunctiveWatch.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/monitoring/PatternWatch.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/monitoring/Watch.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/network/Network.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/Distribution.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/ConfigurationTemplate.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/DatabaseConfiguration.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NetworkInterface.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryConfiguration.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryType.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/UserConfiguration.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/process/JarCommand.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/process/output/OutputListener.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/service/Service.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceInitiator.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceSettings.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/service/database/H2Service.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/service/database/SqlServerService.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/ssh/MonitoringSSHClient.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/ssh/SSHClient.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Database.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Ssh.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Startup.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Substeps.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/CashSteps.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/ConfigurationSteps.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DatabaseSteps.kt delete mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/NetworkSteps.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/RpcSteps.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/SshSteps.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/StartupSteps.kt create mode 100644 experimental/behave/src/scenario/resources/features/cash/currencies.feature create mode 100644 experimental/behave/src/scenario/resources/features/database/connection.feature delete mode 100644 experimental/behave/src/scenario/resources/features/dummy.feature create mode 100644 experimental/behave/src/scenario/resources/features/startup/logging.feature delete mode 100644 experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt create mode 100644 experimental/behave/src/test/kotlin/net/corda/behave/monitoring/MonitoringTests.kt create mode 100644 experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt create mode 100644 experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt create mode 100644 experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt create mode 100644 experimental/behave/src/test/resources/log4j2.xml diff --git a/experimental/behave/README.md b/experimental/behave/README.md new file mode 100644 index 00000000000..163c6310cc3 --- /dev/null +++ b/experimental/behave/README.md @@ -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. \ No newline at end of file diff --git a/experimental/behave/build.gradle b/experimental/behave/build.gradle index 1c420488ce0..569a5deb579 100644 --- a/experimental/behave/build.gradle +++ b/experimental/behave/build.gradle @@ -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 \ No newline at end of file +//scenarios.mustRunAfter test +//scenarios.dependsOn test \ No newline at end of file diff --git a/experimental/behave/deps/.gitignore b/experimental/behave/deps/.gitignore new file mode 100644 index 00000000000..d392f0e82c4 --- /dev/null +++ b/experimental/behave/deps/.gitignore @@ -0,0 +1 @@ +*.jar diff --git a/experimental/behave/deps/corda/3.0.0/.gitkeep b/experimental/behave/deps/corda/3.0.0/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/experimental/behave/deps/corda/3.0.0/apps/.gitkeep b/experimental/behave/deps/corda/3.0.0/apps/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/experimental/behave/deps/drivers/.gitkeep b/experimental/behave/deps/drivers/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/experimental/behave/deps/drivers/README.md b/experimental/behave/deps/drivers/README.md new file mode 100644 index 00000000000..19ff783c5ff --- /dev/null +++ b/experimental/behave/deps/drivers/README.md @@ -0,0 +1,3 @@ +Download and store database drivers here; for example: + - h2-1.4.196.jar + - mssql-jdbc-6.2.2.jre8.jar diff --git a/experimental/behave/prepare.sh b/experimental/behave/prepare.sh new file mode 100755 index 00000000000..62af3afb91c --- /dev/null +++ b/experimental/behave/prepare.sh @@ -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 diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/Utilities.kt b/experimental/behave/src/main/kotlin/net/corda/behave/Utilities.kt new file mode 100644 index 00000000000..1fdc9f9bcc3 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/Utilities.kt @@ -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) diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt b/experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt deleted file mode 100644 index b3dd28f73c9..00000000000 --- a/experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt +++ /dev/null @@ -1,7 +0,0 @@ -package net.corda.behave - -object Utility { - - fun dummy() = true - -} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConfigurationTemplate.kt b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConfigurationTemplate.kt new file mode 100644 index 00000000000..0acff416e84 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConfigurationTemplate.kt @@ -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() + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConnection.kt b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConnection.kt new file mode 100644 index 00000000000..1916000bb23 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConnection.kt @@ -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 + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseSettings.kt b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseSettings.kt new file mode 100644 index 00000000000..4e870f77de0 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseSettings.kt @@ -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() + + 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 { + return serviceInitiators.map { it(config) } + } + + val template: DatabaseConfigurationTemplate + get() = databaseConfigTemplate + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseType.kt b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseType.kt new file mode 100644 index 00000000000..851a8d1387f --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseType.kt @@ -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 + } + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/H2ConfigurationTemplate.kt b/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/H2ConfigurationTemplate.kt new file mode 100644 index 00000000000..6bdfa2ad806 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/H2ConfigurationTemplate.kt @@ -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} + """ + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/SqlServerConfigurationTemplate.kt b/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/SqlServerConfigurationTemplate.kt new file mode 100644 index 00000000000..370a241d202 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/SqlServerConfigurationTemplate.kt @@ -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}" + |} + """ + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/file/FileUtilities.kt b/experimental/behave/src/main/kotlin/net/corda/behave/file/FileUtilities.kt new file mode 100644 index 00000000000..405404fcb21 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/file/FileUtilities.kt @@ -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) diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/file/LogSource.kt b/experimental/behave/src/main/kotlin/net/corda/behave/file/LogSource.kt new file mode 100644 index 00000000000..4649a4898a6 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/file/LogSource.kt @@ -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 { + 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() + 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 + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/logging/LogUtilities.kt b/experimental/behave/src/main/kotlin/net/corda/behave/logging/LogUtilities.kt new file mode 100644 index 00000000000..76a66c9c4e5 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/logging/LogUtilities.kt @@ -0,0 +1,7 @@ +package net.corda.behave.logging + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +inline fun getLogger(): Logger = + LoggerFactory.getLogger(T::class.java) diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/ConjunctiveWatch.kt b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/ConjunctiveWatch.kt new file mode 100644 index 00000000000..c0308fca1e8 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/ConjunctiveWatch.kt @@ -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, timeout: Duration): Boolean { + val latch = CountDownLatch(2) + listOf(left, right).parallelStream().forEach { + if (it.await(observable, timeout)) { + latch.countDown() + } + } + return latch.await(timeout) + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/DisjunctiveWatch.kt b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/DisjunctiveWatch.kt new file mode 100644 index 00000000000..061ca1ed613 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/DisjunctiveWatch.kt @@ -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, timeout: Duration): Boolean { + val latch = CountDownLatch(1) + listOf(left, right).parallelStream().forEach { + if (it.await(observable, timeout)) { + latch.countDown() + } + } + return latch.await(timeout) + } + +} + diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/PatternWatch.kt b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/PatternWatch.kt new file mode 100644 index 00000000000..50a715dd79f --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/PatternWatch.kt @@ -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("") + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/Watch.kt b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/Watch.kt new file mode 100644 index 00000000000..c5b7d949202 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/Watch.kt @@ -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, + 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) + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/network/Network.kt b/experimental/behave/src/main/kotlin/net/corda/behave/network/Network.kt new file mode 100644 index 00000000000..d0cabfc02ce --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/network/Network.kt @@ -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, + private val targetDirectory: File, + private val timeout: Duration = 2.minutes +) : Closeable, Iterable { + + private val log = getLogger() + + 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() + + 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 = 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 { + 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) + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/Distribution.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/Distribution.kt new file mode 100644 index 00000000000..cca3570f398 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/Distribution.kt @@ -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() + + 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 } + } + + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt new file mode 100644 index 00000000000..e5c9f69bb27 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt @@ -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() + + 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() + + 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() + + private var location: String = "London" + + private var country: String = "GB" + + private val apps = mutableListOf() + + 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): 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 error(message: String): T { + throw IllegalArgumentException(message) + } + + } + + companion object { + + fun new() = Builder() + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt new file mode 100644 index 00000000000..ce1cdc44e28 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt @@ -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" + + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/ConfigurationTemplate.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/ConfigurationTemplate.kt new file mode 100644 index 00000000000..332e7de9513 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/ConfigurationTemplate.kt @@ -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() + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt new file mode 100644 index 00000000000..e6a2c94be22 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt @@ -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)}\"" + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt new file mode 100644 index 00000000000..fd639141e0c --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt @@ -0,0 +1,18 @@ +package net.corda.behave.node.configuration + +class CurrencyConfiguration(private val issuableCurrencies: List) : ConfigurationTemplate() { + + override val config: (Configuration) -> String + get() = { + if (issuableCurrencies.isEmpty()) { + "" + } else { + """ + |issuableCurrencies=[ + | ${issuableCurrencies.joinToString(", ")} + |] + """ + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/DatabaseConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/DatabaseConfiguration.kt new file mode 100644 index 00000000000..ca9f3c37a84 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/DatabaseConfiguration.kt @@ -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) + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NetworkInterface.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NetworkInterface.kt new file mode 100644 index 00000000000..fe09792dd2d --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NetworkInterface.kt @@ -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 + } + } + + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryConfiguration.kt new file mode 100644 index 00000000000..58dff6ad6d7 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryConfiguration.kt @@ -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 }" + } + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryType.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryType.kt new file mode 100644 index 00000000000..abceb4b519c --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryType.kt @@ -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 + } +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/UserConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/UserConfiguration.kt new file mode 100644 index 00000000000..1c44a2f9676 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/UserConfiguration.kt @@ -0,0 +1,37 @@ +package net.corda.behave.node.configuration + +class UserConfiguration : ConfigurationTemplate(), Iterable { + + data class User(val username: String, val password: String, val permissions: List) + + private val users = mutableListOf() + + fun withUser(username: String, password: String, permissions: List = listOf("ALL")): UserConfiguration { + users.add(User(username, password, permissions)) + return this + } + + override fun iterator(): Iterator { + 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() + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt b/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt new file mode 100644 index 00000000000..a35ae287db6 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt @@ -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, + private val directory: File = currentDirectory, + private val timeout: Duration = 2.minutes +): Closeable { + + protected val log = getLogger() + + 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 = Observable.create { 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) -> 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 + + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/process/JarCommand.kt b/experimental/behave/src/main/kotlin/net/corda/behave/process/JarCommand.kt new file mode 100644 index 00000000000..d465170c17b --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/process/JarCommand.kt @@ -0,0 +1,34 @@ +package net.corda.behave.process + +import java.io.File +import java.time.Duration + +class JarCommand( + jarFile: File, + arguments: Array, + 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() + } + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/process/output/OutputListener.kt b/experimental/behave/src/main/kotlin/net/corda/behave/process/output/OutputListener.kt new file mode 100644 index 00000000000..18dcc065e68 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/process/output/OutputListener.kt @@ -0,0 +1,9 @@ +package net.corda.behave.process.output + +interface OutputListener { + + fun onNewLine(line: String) + + fun onEndOfStream() + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt new file mode 100644 index 00000000000..262c0fcb27c --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt @@ -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 = 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() + } + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/Service.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/Service.kt new file mode 100644 index 00000000000..64c7e869f83 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/Service.kt @@ -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() + + 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 + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceInitiator.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceInitiator.kt new file mode 100644 index 00000000000..eb3d042d7fd --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceInitiator.kt @@ -0,0 +1,5 @@ +package net.corda.behave.service + +import net.corda.behave.node.configuration.Configuration + +typealias ServiceInitiator = (Configuration) -> Service diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceSettings.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceSettings.kt new file mode 100644 index 00000000000..f94523691ee --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceSettings.kt @@ -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 +) diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/database/H2Service.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/database/H2Service.kt new file mode 100644 index 00000000000..484944ee290 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/database/H2Service.kt @@ -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" + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/database/SqlServerService.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/database/SqlServerService.kt new file mode 100644 index 00000000000..6a18df586f7 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/database/SqlServerService.kt @@ -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" + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/ssh/MonitoringSSHClient.kt b/experimental/behave/src/main/kotlin/net/corda/behave/ssh/MonitoringSSHClient.kt new file mode 100644 index 00000000000..fcbde2d54f6 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/ssh/MonitoringSSHClient.kt @@ -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 = Observable.create { 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) + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/ssh/SSHClient.kt b/experimental/behave/src/main/kotlin/net/corda/behave/ssh/SSHClient.kt new file mode 100644 index 00000000000..afa530cf3c4 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/ssh/SSHClient.kt @@ -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() + 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() + + 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 = ;\n" + + "\teol2 = ; 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") + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt index 745ef851b67..f3fad092c91 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt @@ -12,6 +12,7 @@ class ScenarioHooks(private val state: ScenarioState) { @After fun afterScenario() { + state.stopNetwork() } } \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt index a21ab59a108..02e2af9c224 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt @@ -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() + + private val nodes = mutableListOf() + + private var network: Network? = null + + fun fail(message: String) { + error(message) + } + + fun 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 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 + } } \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt index 69ef2938538..818c08916a0 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt @@ -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 error(message: String) = state.error(message) + + fun node(name: String) = state.nodeBuilder(name) + + fun withNetwork(action: ScenarioState.() -> Unit) { + state.withNetwork(action) + } + + fun 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) } diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt new file mode 100644 index 00000000000..f119e5ebe0d --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt @@ -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 + } + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Database.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Database.kt new file mode 100644 index 00000000000..2406aff6753 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Database.kt @@ -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() + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Ssh.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Ssh.kt new file mode 100644 index 00000000000..a1e7ddd875b --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Ssh.kt @@ -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() + 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") + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Startup.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Startup.kt new file mode 100644 index 00000000000..3520acec431 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Startup.kt @@ -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") + + } + } + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Substeps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Substeps.kt new file mode 100644 index 00000000000..bba2f052e05 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Substeps.kt @@ -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() + + protected fun withNetwork(action: ScenarioState.() -> Unit) = + state.withNetwork(action) + + protected fun withClient(nodeName: String, action: ScenarioState.(CordaRPCOps) -> T): T { + return state.withClient(nodeName, { + return@withClient try { + action(state, it) + } catch (ex: Exception) { + state.error(ex.message ?: "Failed to execute RPC call") + } + }) + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/CashSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/CashSteps.kt new file mode 100644 index 00000000000..e26486c3515 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/CashSteps.kt @@ -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("^node (\\w+) has 1 issuable currency$") { name -> + withNetwork { + assertThat(cash.numberOfIssuableCurrencies(name)).isEqualTo(1) + } + } + + Then("^node (\\w+) has (\\w+) issuable currencies$") { name, count -> + withNetwork { + assertThat(cash.numberOfIssuableCurrencies(name)).isEqualTo(count.toInt()) + } + } + +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/ConfigurationSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/ConfigurationSteps.kt new file mode 100644 index 00000000000..27def111b8e --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/ConfigurationSteps.kt @@ -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("^a node (\\w+) of version ([^ ]+)$") { name, version -> + node(name) + .withDistribution(Distribution.fromVersionString(version) + ?: error("Unknown version '$version'")) + } + + Given("^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("^node (\\w+) uses database of type (.+)$") { name, type -> + node(name) + .withDatabaseType(DatabaseType.fromName(type) + ?: error("Unknown database type '$type'")) + } + + Given("^node (\\w+) can issue (.+)$") { name, currencies -> + node(name).withIssuableCurrencies(currencies + .replace(" and ", ", ") + .split(", ") + .map { it.toUpperCase() }) + } + + Given("^node (\\w+) is located in (\\w+), (\\w+)$") { name, location, country -> + node(name).withLocation(location, country) + } + + Given("^node (\\w+) has the finance app installed$") { name -> + node(name).withFinanceApp() + } + + Given("^node (\\w+) has app installed: (.+)$") { name, app -> + node(name).withApp(app) + } + +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DatabaseSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DatabaseSteps.kt new file mode 100644 index 00000000000..9b21650a509 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DatabaseSteps.kt @@ -0,0 +1,13 @@ +package net.corda.behave.scenarios.steps + +import net.corda.behave.scenarios.StepsBlock + +fun databaseSteps(steps: StepsBlock) = steps { + + Then("^user can connect to the database of node (\\w+)$") { name -> + withNetwork { + database.canConnectTo(name) + } + } + +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt deleted file mode 100644 index ce86fa51866..00000000000 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt +++ /dev/null @@ -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("^(\\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}") - } - -} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/NetworkSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/NetworkSteps.kt new file mode 100644 index 00000000000..fcc544de29b --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/NetworkSteps.kt @@ -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() + } + +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/RpcSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/RpcSteps.kt new file mode 100644 index 00000000000..9accb283988 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/RpcSteps.kt @@ -0,0 +1,13 @@ +package net.corda.behave.scenarios.steps + +import net.corda.behave.scenarios.StepsBlock + +fun rpcSteps(steps: StepsBlock) = steps { + + Then("^user can connect to node (\\w+) using RPC$") { name -> + withClient(name) { + succeed() + } + } + +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/SshSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/SshSteps.kt new file mode 100644 index 00000000000..516732f1e79 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/SshSteps.kt @@ -0,0 +1,13 @@ +package net.corda.behave.scenarios.steps + +import net.corda.behave.scenarios.StepsBlock + +fun sshSteps(steps: StepsBlock) = steps { + + Then("^user can connect to node (\\w+) using SSH$") { name -> + withNetwork { + ssh.canConnectTo(name) + } + } + +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/StartupSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/StartupSteps.kt new file mode 100644 index 00000000000..f78415f2c85 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/StartupSteps.kt @@ -0,0 +1,31 @@ +package net.corda.behave.scenarios.steps + +import net.corda.behave.scenarios.StepsBlock + +fun startupSteps(steps: StepsBlock) = steps { + + Then("^user can retrieve database details for node (\\w+)$") { name -> + withNetwork { + startup.hasDatabaseDetails(name) + } + } + + Then("^user can retrieve logging information for node (\\w+)$") { name -> + withNetwork { + startup.hasLoggingInformation(name) + } + } + + Then("^node (\\w+) is on version ([^ ]+)$") { name, version -> + withNetwork { + startup.hasVersion(name, version) + } + } + + Then("^node (\\w+) is on platform version (\\w+)$") { name, platformVersion -> + withNetwork { + startup.hasPlatformVersion(name, platformVersion.toInt()) + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/resources/features/cash/currencies.feature b/experimental/behave/src/scenario/resources/features/cash/currencies.feature new file mode 100644 index 00000000000..b90e7fa8a17 --- /dev/null +++ b/experimental/behave/src/scenario/resources/features/cash/currencies.feature @@ -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 \ No newline at end of file diff --git a/experimental/behave/src/scenario/resources/features/database/connection.feature b/experimental/behave/src/scenario/resources/features/database/connection.feature new file mode 100644 index 00000000000..47137b8aeab --- /dev/null +++ b/experimental/behave/src/scenario/resources/features/database/connection.feature @@ -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 + And node A uses database of 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 | \ No newline at end of file diff --git a/experimental/behave/src/scenario/resources/features/dummy.feature b/experimental/behave/src/scenario/resources/features/dummy.feature deleted file mode 100644 index 6ff1613bd55..00000000000 --- a/experimental/behave/src/scenario/resources/features/dummy.feature +++ /dev/null @@ -1,6 +0,0 @@ -Feature: Dummy - Lorem ipsum - - Scenario: Noop - Given 15 dummies exist - Then there is a dummy \ No newline at end of file diff --git a/experimental/behave/src/scenario/resources/features/startup/logging.feature b/experimental/behave/src/scenario/resources/features/startup/logging.feature new file mode 100644 index 00000000000..8cbf35eda18 --- /dev/null +++ b/experimental/behave/src/scenario/resources/features/startup/logging.feature @@ -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 diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt deleted file mode 100644 index c956cc67e1f..00000000000 --- a/experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt +++ /dev/null @@ -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()) - } - -} \ No newline at end of file diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/monitoring/MonitoringTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/monitoring/MonitoringTests.kt new file mode 100644 index 00000000000..23e0718b6d6 --- /dev/null +++ b/experimental/behave/src/test/kotlin/net/corda/behave/monitoring/MonitoringTests.kt @@ -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() + } + +} \ No newline at end of file diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt new file mode 100644 index 00000000000..93cdaa9394d --- /dev/null +++ b/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt @@ -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) + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt new file mode 100644 index 00000000000..a6a6b121a75 --- /dev/null +++ b/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt @@ -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() + 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) + } + +} \ No newline at end of file diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt new file mode 100644 index 00000000000..79a662a6171 --- /dev/null +++ b/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt @@ -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() + } + +} \ No newline at end of file diff --git a/experimental/behave/src/test/resources/log4j2.xml b/experimental/behave/src/test/resources/log4j2.xml new file mode 100644 index 00000000000..43fcf63c3d5 --- /dev/null +++ b/experimental/behave/src/test/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file From bd707eb9c88235f48c39738d12e66b54f6a15a67 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 9 Feb 2018 19:23:56 +0000 Subject: [PATCH 3/8] Update README --- experimental/behave/README.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/experimental/behave/README.md b/experimental/behave/README.md index 163c6310cc3..33c5517cf72 100644 --- a/experimental/behave/README.md +++ b/experimental/behave/README.md @@ -1,3 +1,29 @@ +# Introduction + +This project illustrates how one can use Cucumber / BDD to drive +and test homogeneous and heterogeneous Corda networks on a local +machine. The framework has built-in support for Dockerised node +dependencies so that you easily can spin up a Corda node locally +that, for instance, uses a 3rd party database provider such as +MS SQL Server or Postgres. + +# Structure + +The project is split into three pieces: + + * **Testing Library** (main) - This library contains auxiliary + functions that help in configuring and bootstrapping Corda + networks on a local machine. The purpose of the library is to + aid in black-box testing and automation. + + * **Unit Tests** (test) - These are various tests for the + library described above. Note that there's only limited + coverage for now. + + * **BDD Framework** (scenario) - This module shows how to use + BDD-style frameworks to control the testing of Corda networks; + more specifically, using [Cucumber](cucumber.io). + # Setup To get started, please run the following command: @@ -6,6 +32,6 @@ To get started, please run the following command: $ ./prepare.sh ``` -This command will download necessary database drivers and set up +This script will download necessary database drivers and set up the dependencies directory with copies of the Corda fat-JAR and -the network bootstrapping tool. \ No newline at end of file +the network bootstrapping tool. From d5d5b12a2db38446bc1e7c8e3d273691bbdef088 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 9 Feb 2018 19:31:03 +0000 Subject: [PATCH 4/8] Ignore tests to not impede test pipeline --- .../src/test/kotlin/net/corda/behave/network/NetworkTests.kt | 3 +++ .../kotlin/net/corda/behave/service/SqlServerServiceTests.kt | 2 ++ 2 files changed, 5 insertions(+) diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt index 93cdaa9394d..93801b1d8d7 100644 --- a/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt +++ b/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt @@ -3,10 +3,12 @@ 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.Ignore import org.junit.Test class NetworkTests { + @Ignore @Test fun `network of two nodes can be spun up`() { val network = Network @@ -21,6 +23,7 @@ class NetworkTests { } } + @Ignore @Test fun `network of three nodes and mixed databases can be spun up`() { val network = Network diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt index 79a662a6171..872574cb70a 100644 --- a/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt +++ b/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt @@ -2,10 +2,12 @@ package net.corda.behave.service import net.corda.behave.service.database.SqlServerService import org.assertj.core.api.Assertions.assertThat +import org.junit.Ignore import org.junit.Test class SqlServerServiceTests { + @Ignore @Test fun `sql server can be started and stopped`() { val service = SqlServerService("test-mssql", 12345, "S0meS3cretW0rd") From c2503921ad063adc594618179e559a1f1a108430 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Mon, 12 Feb 2018 10:07:27 +0000 Subject: [PATCH 5/8] Update instructions --- experimental/behave/README.md | 18 ++++++++++++++---- experimental/behave/prepare.sh | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/experimental/behave/README.md b/experimental/behave/README.md index 33c5517cf72..0e10434285e 100644 --- a/experimental/behave/README.md +++ b/experimental/behave/README.md @@ -26,11 +26,21 @@ The project is split into three pieces: # Setup -To get started, please run the following command: +To get started, please follow the instructions below: -```bash -$ ./prepare.sh -``` + * Go up to the root directory and build the capsule JAR. + + ```bash + $ cd ../../ + $ ./gradlew install + ``` + + * Come back to this folder and run: + + ```bash + $ cd experimental/behave + $ ./prepare.sh + ``` This script will download necessary database drivers and set up the dependencies directory with copies of the Corda fat-JAR and diff --git a/experimental/behave/prepare.sh b/experimental/behave/prepare.sh index 62af3afb91c..3912cf11bef 100755 --- a/experimental/behave/prepare.sh +++ b/experimental/behave/prepare.sh @@ -7,7 +7,7 @@ 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 +cp $(ls ../../node/capsule/build/libs/corda-*.jar | tail -n1) 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 From cca71e3d6eae39664f649cbe13a49fe9dd4953a0 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Mon, 12 Feb 2018 10:18:18 +0000 Subject: [PATCH 6/8] Make RPC scaffolding available for Node --- .../main/kotlin/net/corda/behave/node/Node.kt | 22 ++++++++++++++++ .../corda/behave/scenarios/ScenarioState.kt | 26 ++++--------------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt index e5c9f69bb27..ed6b9f097ad 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt @@ -9,10 +9,15 @@ 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.seconds import net.corda.behave.service.Service import net.corda.behave.service.ServiceSettings import net.corda.behave.ssh.MonitoringSSHClient import net.corda.behave.ssh.SSHClient +import net.corda.client.rpc.CordaRPCClient +import net.corda.client.rpc.CordaRPCClientConfiguration +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.utilities.NetworkHostAndPort import org.apache.commons.io.FileUtils import java.io.File import java.time.Duration @@ -146,6 +151,23 @@ class Node( }).start() } + fun rpc(action: (CordaRPCOps) -> T): T { + var result: T? = null + val user = config.users.first() + val address = config.nodeInterface + val targetHost = NetworkHostAndPort(address.host, address.rpcPort) + val config = CordaRPCClientConfiguration( + connectionMaxRetryInterval = 10.seconds + ) + log.info("Establishing RPC connection to ${targetHost.host} on port ${targetHost.port} ...") + CordaRPCClient(targetHost, config).use(user.username, user.password) { + log.info("RPC connection to ${targetHost.host}:${targetHost.port} established") + val client = it.proxy + result = action(client) + } + return result ?: error("Failed to run RPC action") + } + override fun toString(): String { return "Node(name = ${config.name}, version = ${config.distribution.version})" } diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt index 02e2af9c224..f6cfb32298d 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt @@ -3,11 +3,7 @@ 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 { @@ -54,29 +50,17 @@ class ScenarioState { assertThat(network?.waitUntilRunning()).isTrue() } - fun withNetwork(action: ScenarioState.() -> Unit) { + inline fun withNetwork(action: ScenarioState.() -> T): T { ensureNetworkIsRunning() - action() + return action() } - fun withClient(nodeName: String, action: (CordaRPCOps) -> T): T { - var result: T? = null + inline fun withClient(nodeName: String, crossinline action: (CordaRPCOps) -> T): T { 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 node(nodeName).rpc { + action(it) } } - return result ?: error("Failed to run RPC action") } fun stopNetwork() { From 74e65f392f3e3b5b85761aac8d62f152ad827f74 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Mon, 12 Feb 2018 10:36:44 +0000 Subject: [PATCH 7/8] Fix prepare script and cash config check --- experimental/behave/prepare.sh | 6 +++--- .../kotlin/net/corda/behave/scenarios/helpers/Cash.kt | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/experimental/behave/prepare.sh b/experimental/behave/prepare.sh index 3912cf11bef..02c5df162bc 100755 --- a/experimental/behave/prepare.sh +++ b/experimental/behave/prepare.sh @@ -7,7 +7,7 @@ mkdir -p deps/corda/${VERSION}/apps mkdir -p deps/drivers # Copy Corda capsule into deps -cp $(ls ../../node/capsule/build/libs/corda-*.jar | tail -n1) deps/corda/${VERSION}/corda.jar +cp -v $(ls ../../node/capsule/build/libs/corda-*.jar | tail -n1) 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 @@ -20,5 +20,5 @@ cd ../.. # 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 +cp -v $(ls ../../tools/bootstrapper/build/libs/*.jar | tail -n1) deps/corda/${VERSION}/network-bootstrapper.jar +cp -v $(ls ../../finance/build/libs/corda-finance-*.jar | tail -n1) deps/corda/${VERSION}/apps/corda-finance.jar diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt index f119e5ebe0d..0b597711646 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt @@ -21,8 +21,9 @@ class Cash(state: ScenarioState) : Substeps(state) { log.info("Can issue $issuableCurrency") } return@withClient config.issuableCurrencies.size - } catch (_: Exception) { - return@withClient 0 + } catch (ex: Exception) { + log.warn("Failed to retrieve cash configuration data", ex) + throw ex } } } From 407885c93b66b426838384fc71eaaba5dbf6263e Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Mon, 12 Feb 2018 10:43:54 +0000 Subject: [PATCH 8/8] Enable selective runs --- experimental/behave/README.md | 11 +++++++++++ experimental/behave/build.gradle | 5 +++++ experimental/behave/src/scenario/kotlin/Scenarios.kt | 1 - .../resources/features/cash/currencies.feature | 1 + .../resources/features/database/connection.feature | 1 + .../resources/features/startup/logging.feature | 12 ++++++++++-- .../kotlin/net/corda/behave/process/CommandTests.kt | 2 +- 7 files changed, 29 insertions(+), 4 deletions(-) diff --git a/experimental/behave/README.md b/experimental/behave/README.md index 0e10434285e..b3b37ca2e97 100644 --- a/experimental/behave/README.md +++ b/experimental/behave/README.md @@ -45,3 +45,14 @@ To get started, please follow the instructions below: This script will download necessary database drivers and set up the dependencies directory with copies of the Corda fat-JAR and the network bootstrapping tool. + +# Selective Runs + +If you only want to run tests of a specific tag, you can append +the following parameter to the Gradle command: + +```bash +$ ../../gradlew scenario -Ptags="@cash" +# or +$ ../../gradlew scenario -Ptags="@cash,@logging" +``` \ No newline at end of file diff --git a/experimental/behave/build.gradle b/experimental/behave/build.gradle index 569a5deb579..4148d9d4626 100644 --- a/experimental/behave/build.gradle +++ b/experimental/behave/build.gradle @@ -108,6 +108,11 @@ task scenarios(type: Test) { setTestClassesDirs sourceSets.scenario.output.getClassesDirs() classpath = sourceSets.scenario.runtimeClasspath outputs.upToDateWhen { false } + + if (project.hasProperty("tags")) { + systemProperty "cucumber.options", "--tags $tags" + logger.warn("Only running tests tagged with: $tags ...") + } } //scenarios.mustRunAfter test diff --git a/experimental/behave/src/scenario/kotlin/Scenarios.kt b/experimental/behave/src/scenario/kotlin/Scenarios.kt index b0c96a98ee8..ac65923a005 100644 --- a/experimental/behave/src/scenario/kotlin/Scenarios.kt +++ b/experimental/behave/src/scenario/kotlin/Scenarios.kt @@ -4,7 +4,6 @@ import org.junit.runner.RunWith @RunWith(Cucumber::class) @CucumberOptions( - features = arrayOf("src/scenario/resources/features"), glue = arrayOf("net.corda.behave.scenarios"), plugin = arrayOf("pretty") ) diff --git a/experimental/behave/src/scenario/resources/features/cash/currencies.feature b/experimental/behave/src/scenario/resources/features/cash/currencies.feature index b90e7fa8a17..3085ced84ee 100644 --- a/experimental/behave/src/scenario/resources/features/cash/currencies.feature +++ b/experimental/behave/src/scenario/resources/features/cash/currencies.feature @@ -1,3 +1,4 @@ +@cash @issuance Feature: Cash - Issuable Currencies To have cash on ledger, certain nodes must have the ability to issue cash of various currencies. diff --git a/experimental/behave/src/scenario/resources/features/database/connection.feature b/experimental/behave/src/scenario/resources/features/database/connection.feature index 47137b8aeab..9be4fc7d2d5 100644 --- a/experimental/behave/src/scenario/resources/features/database/connection.feature +++ b/experimental/behave/src/scenario/resources/features/database/connection.feature @@ -1,3 +1,4 @@ +@database @connectivity Feature: Database - Connection For Corda to work, a database must be running and appropriately configured. diff --git a/experimental/behave/src/scenario/resources/features/startup/logging.feature b/experimental/behave/src/scenario/resources/features/startup/logging.feature index 8cbf35eda18..20531237c6d 100644 --- a/experimental/behave/src/scenario/resources/features/startup/logging.feature +++ b/experimental/behave/src/scenario/resources/features/startup/logging.feature @@ -1,3 +1,4 @@ +@logging @startup Feature: Startup Information - Logging A Corda node should inform the user of important parameters during startup so that he/she can confirm the setup and configure / connect relevant software to said node. @@ -7,7 +8,14 @@ Feature: Startup Information - Logging And node A uses database of type H2 And node A is located in London, GB When the network is ready + Then user can retrieve logging information for node A + + Scenario: Node shows database details on startup + Given a node A of version MASTER + When the network is ready + Then user can retrieve database details for node A + + Scenario: Node shows version information on startup + Given a node A of version MASTER Then node A is on platform version 2 And node A is on version 3.0-SNAPSHOT - And user can retrieve logging information for node A - And user can retrieve database details for node A diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt index a6a6b121a75..4395ddb83a0 100644 --- a/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt +++ b/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt @@ -14,7 +14,7 @@ class CommandTests { @Test fun `failed command returns non-zero`() { - val exitCode = Command(listOf("some-random-command-that-does-not-exist")).run() + val exitCode = Command(listOf("ls", "some-weird-path-that-does-not-exist")).run() assertThat(exitCode).isNotEqualTo(0) }