From 9632ec157fc9c3c31455c08b60ae40cb91ac23f4 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Fri, 12 May 2017 10:27:41 +0100 Subject: [PATCH] Add simple smoke tests for CordaRPCClient. (#667) --- client/rpc/build.gradle | 36 +++- .../kotlin/net/corda/kotlin/rpc/NodeConfig.kt | 53 ++++++ .../net/corda/kotlin/rpc/NodeProcess.kt | 100 ++++++++++++ .../kotlin/rpc/StandaloneCordaRPClientTest.kt | 154 ++++++++++++++++++ 4 files changed, 341 insertions(+), 2 deletions(-) create mode 100644 client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeConfig.kt create mode 100644 client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeProcess.kt create mode 100644 client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt diff --git a/client/rpc/build.gradle b/client/rpc/build.gradle index c6ad9c5c69..6cb5f4a67e 100644 --- a/client/rpc/build.gradle +++ b/client/rpc/build.gradle @@ -11,6 +11,9 @@ configurations { integrationTestCompile.extendsFrom testCompile integrationTestRuntime.extendsFrom testRuntime + + smokeTestCompile.extendsFrom compile + smokeTestRuntime.extendsFrom runtime } sourceSets { @@ -26,6 +29,24 @@ sourceSets { srcDir "../../config/test" } } + smokeTest { + kotlin { + // We must NOT have any Node code on the classpath, so do NOT + // include the test or integrationTest dependencies here. + compileClasspath += main.output + runtimeClasspath += main.output + srcDir file('src/smoke-test/kotlin') + } + } +} + +processSmokeTestResources { + from(file("$rootDir/config/test/log4j2.xml")) { + rename 'log4j2\\.xml', 'log4j2-test.xml' + } + from(project(':node:capsule').tasks.buildCordaJAR) { + rename 'corda-(.*)', 'corda.jar' + } } // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in @@ -42,11 +63,22 @@ dependencies { testCompile project(':test-utils') - // Integration test helpers - integrationTestCompile "junit:junit:$junit_version" + // Smoke tests do NOT have any Node code on the classpath! + smokeTestCompile project(':finance') + smokeTestCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + smokeTestCompile "org.apache.logging.log4j:log4j-core:$log4j_version" + smokeTestCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + smokeTestCompile "org.assertj:assertj-core:${assertj_version}" + smokeTestCompile "junit:junit:$junit_version" } task integrationTest(type: Test) { testClassesDir = sourceSets.integrationTest.output.classesDir classpath = sourceSets.integrationTest.runtimeClasspath } + +task smokeTest(type: Test) { + testClassesDir = sourceSets.smokeTest.output.classesDir + classpath = sourceSets.smokeTest.runtimeClasspath + systemProperties['build.dir'] = buildDir +} diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeConfig.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeConfig.kt new file mode 100644 index 0000000000..4358928358 --- /dev/null +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeConfig.kt @@ -0,0 +1,53 @@ +package net.corda.kotlin.rpc + +import com.typesafe.config.* +import net.corda.core.crypto.Party +import net.corda.nodeapi.User + +class NodeConfig( + val party: Party, + val p2pPort: Int, + val rpcPort: Int, + val webPort: Int, + val extraServices: List, + val users: List, + var networkMap: NodeConfig? = null +) { + companion object { + val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) + } + + val commonName: String = party.name + + /* + * The configuration object depends upon the networkMap, + * which is mutable. + */ + fun toFileConfig(): Config = ConfigFactory.empty() + .withValue("myLegalName", valueFor(party.name)) + .withValue("p2pAddress", addressValueFor(p2pPort)) + .withValue("extraAdvertisedServiceIds", valueFor(extraServices)) + .withFallback(optional("networkMapService", networkMap, { c, n -> + c.withValue("address", addressValueFor(n.p2pPort)) + .withValue("legalName", valueFor(n.party.name)) + })) + .withValue("webAddress", addressValueFor(webPort)) + .withValue("rpcAddress", addressValueFor(rpcPort)) + .withValue("rpcUsers", valueFor(users.map(User::toMap).toList())) + .withValue("useTestClock", valueFor(true)) + + fun toText(): String = toFileConfig().root().render(renderOptions) + + private fun valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any) + private fun addressValueFor(port: Int) = valueFor("localhost:$port") + private inline fun optional(path: String, obj: T?, body: (Config, T) -> Config): Config { + val config = ConfigFactory.empty() + return if (obj == null) config else body(config, obj).atPath(path) + } +} + +private fun User.toMap(): Map = mapOf( + "username" to username, + "password" to password, + "permissions" to permissions +) diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeProcess.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeProcess.kt new file mode 100644 index 0000000000..ab5088083d --- /dev/null +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeProcess.kt @@ -0,0 +1,100 @@ +package net.corda.kotlin.rpc + +import com.google.common.net.HostAndPort +import net.corda.client.rpc.CordaRPCClient +import net.corda.core.utilities.loggerFor +import java.io.File +import java.net.URI +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit.SECONDS +import kotlin.test.* + +class NodeProcess( + val config: NodeConfig, + val nodeDir: Path, + private val node: Process, + private val client: CordaRPCClient +) : AutoCloseable { + private companion object { + val log = loggerFor() + val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java") + val corda: URI = this::class.java.getResource("/corda.jar").toURI() + val buildDir: Path = Paths.get(System.getProperty("build.dir")) + val capsuleDir: Path = buildDir.resolve("capsule") + } + + fun connect(): CordaRPCClient { + val user = config.users[0] + return client.start(user.username, user.password) + } + + override fun close() { + node.destroy() + val isDead = node.waitFor(60, SECONDS) + assertTrue(isDead, "Node '${config.commonName}' has not shutdown correctly") + + log.info("Deleting Artemis directories, because they're large!") + nodeDir.resolve("artemis").toFile().deleteRecursively() + } + + class Factory(val nodesDir: Path) { + init { + assertTrue(nodesDir.toFile().forceDirectory(), "Nodes directory does not exist") + } + + fun create(config: NodeConfig): NodeProcess { + val nodeDir = Files.createTempDirectory(nodesDir, config.commonName) + log.info("Node directory: {}", nodeDir) + + val confFile = nodeDir.resolve("node.conf").toFile() + confFile.writeText(config.toText()) + + val process = startNode(nodeDir) + val client = CordaRPCClient(HostAndPort.fromParts("localhost", config.rpcPort)) + val user = config.users[0] + + val setupExecutor = Executors.newSingleThreadScheduledExecutor() + try { + setupExecutor.scheduleWithFixedDelay({ + try { + val conn = client.start(user.username, user.password) + conn.close() + + // Cancel the "setup" task now that we've created the RPC client. + setupExecutor.shutdown() + } catch (e: Exception) { + log.warn("Node '{}' not ready yet (Error: {})", config.commonName, e.message) + } + }, 5, 1, SECONDS) + + val setupOK = setupExecutor.awaitTermination(120, SECONDS) + assertTrue(setupOK, "Failed to create RPC connection") + } catch (e: Exception) { + process.destroy() + throw e + } finally { + setupExecutor.shutdownNow() + } + + return NodeProcess(config, nodeDir, process, client) + } + + private fun startNode(nodeDir: Path): Process { + val builder = ProcessBuilder() + .command(javaPath.toString(), "-jar", corda.path) + .directory(nodeDir.toFile()) + + builder.environment().putAll(mapOf( + "CAPSULE_CACHE_DIR" to capsuleDir.toString() + )) + + return builder.start() + } + } +} + +private fun File.forceDirectory(): Boolean = this.isDirectory || this.mkdirs() + diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt new file mode 100644 index 0000000000..9603be7920 --- /dev/null +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -0,0 +1,154 @@ +package net.corda.kotlin.rpc + +import net.corda.client.rpc.CordaRPCClient +import java.io.FilterInputStream +import java.io.InputStream +import java.nio.file.Path +import java.nio.file.Paths +import java.time.Duration.ofSeconds +import java.util.Currency +import kotlin.test.* +import net.corda.client.rpc.notUsed +import net.corda.core.contracts.* +import net.corda.core.crypto.Party +import net.corda.core.getOrThrow +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.StateMachineUpdate +import net.corda.core.messaging.startFlow +import net.corda.core.messaging.startTrackedFlow +import net.corda.core.serialization.OpaqueBytes +import net.corda.core.sizedInputStreamAndHash +import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.utilities.loggerFor +import net.corda.flows.CashIssueFlow +import net.corda.nodeapi.User +import org.junit.After +import org.junit.Before +import org.junit.Test + +class StandaloneCordaRPClientTest { + private companion object { + val log = loggerFor() + val buildDir: Path = Paths.get(System.getProperty("build.dir")) + val nodesDir: Path = buildDir.resolve("nodes") + val user = User("user1", "test", permissions = setOf("ALL")) + val factory = NodeProcess.Factory(nodesDir) + const val attachmentSize = 2116 + const val timeout = 60L + } + + private lateinit var notary: NodeProcess + private lateinit var rpcProxy: CordaRPCOps + private lateinit var client: CordaRPCClient + private lateinit var notaryIdentity: Party + + private val notaryConfig = NodeConfig( + party = DUMMY_NOTARY, + p2pPort = 10002, + rpcPort = 10003, + webPort = 10004, + extraServices = listOf("corda.notary.validating"), + users = listOf(user) + ) + + @Before + fun setUp() { + notary = factory.create(notaryConfig) + client = notary.connect() + rpcProxy = client.proxy() + notaryIdentity = fetchNotaryIdentity() + } + + @After + fun done() { + client.close() + notary.close() + } + + @Test + fun `test attachment upload`() { + val attachment = sizedInputStreamAndHash(attachmentSize) + + assertFalse(rpcProxy.attachmentExists(attachment.sha256)) + val id = WrapperStream(attachment.inputStream).use { rpcProxy.uploadAttachment(it) } + assertEquals(id, attachment.sha256, "Attachment has incorrect SHA256 hash") + } + + @Test + fun `test starting flow`() { + rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) + .returnValue.getOrThrow(ofSeconds(timeout)) + } + + @Test + fun `test starting tracked flow`() { + var trackCount = 0 + val handle = rpcProxy.startTrackedFlow( + ::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity + ) + handle.progress.subscribe { msg -> + log.info("Flow>> $msg") + ++trackCount + } + handle.returnValue.getOrThrow(ofSeconds(timeout)) + assertNotEquals(0, trackCount) + } + + @Test + fun `test network map`() { + assertEquals(DUMMY_NOTARY.name, notaryIdentity.name) + } + + @Test + fun `test state machines`() { + val (stateMachines, updates) = rpcProxy.stateMachinesAndUpdates() + assertEquals(0, stateMachines.size) + + var updateCount = 0 + updates.subscribe { update -> + if (update is StateMachineUpdate.Added) { + log.info("StateMachine>> Id=${update.id}") + ++updateCount + } + } + + // Now issue some cash + rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) + .returnValue.getOrThrow(ofSeconds(timeout)) + assertEquals(1, updateCount) + } + + @Test + fun `test vault`() { + val (vault, vaultUpdates) = rpcProxy.vaultAndUpdates() + assertEquals(0, vault.size) + + var updateCount = 0 + vaultUpdates.subscribe { update -> + log.info("Vault>> FlowId=${update.flowId}") + ++updateCount + } + + // Now issue some cash + rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) + .returnValue.getOrThrow(ofSeconds(timeout)) + assertNotEquals(0, updateCount) + + // Check that this cash exists in the vault + val cashBalance = rpcProxy.getCashBalances() + log.info("Cash Balances: $cashBalance") + assertEquals(1, cashBalance.size) + assertEquals(629.POUNDS, cashBalance.get(Currency.getInstance("GBP"))) + } + + + private fun fetchNotaryIdentity(): Party { + val (nodeInfo, nodeUpdates) = rpcProxy.networkMapUpdates() + nodeUpdates.notUsed() + assertEquals(1, nodeInfo.size) + return nodeInfo[0].legalIdentity + } + + // This InputStream cannot have been whitelisted. + private class WrapperStream(input: InputStream) : FilterInputStream(input) +}