CORDA-1892 CRaSH shell flow start fix for similar flow names. (#3874)

* Added fix and associated unit test.

* Fixed broken unit test + added another test case + used NoOpFlows + use Mock output object to assert correct result output.

* Remove unnecessary additional println.

* Minor cleanup in test code.

* Relax nameFragment matching to cater for fully qualified and simple Flow classname specifications.

* Remove superfluous check.

* Minor fix + added additional Unit Test cases to cover all scenarios.

* Reverted back to original behaviour + extra check to avoid ambiguity for exact match.

* Changes following final PR review comments.
This commit is contained in:
josecoll 2018-09-11 11:41:45 +01:00 committed by GitHub
parent ec55397335
commit 37c7fff8b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 173 additions and 5 deletions

View File

@ -3,10 +3,16 @@ package net.corda.tools.shell
import com.google.common.io.Files import com.google.common.io.Files
import com.jcraft.jsch.ChannelExec import com.jcraft.jsch.ChannelExec
import com.jcraft.jsch.JSch import com.jcraft.jsch.JSch
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doAnswer
import com.nhaarman.mockito_kotlin.mock
import net.corda.client.rpc.RPCException import net.corda.client.rpc.RPCException
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.node.services.Permissions import net.corda.node.services.Permissions
import net.corda.node.services.Permissions.Companion.all import net.corda.node.services.Permissions.Companion.all
@ -21,10 +27,12 @@ import net.corda.testing.driver.driver
import net.corda.testing.driver.internal.NodeHandleInternal import net.corda.testing.driver.internal.NodeHandleInternal
import net.corda.testing.internal.useSslRpcOverrides import net.corda.testing.internal.useSslRpcOverrides
import net.corda.testing.node.User import net.corda.testing.node.User
import net.corda.tools.shell.utlities.ANSIProgressRenderer
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.bouncycastle.util.io.Streams import org.bouncycastle.util.io.Streams
import org.crsh.text.RenderPrintWriter
import org.junit.Ignore import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -236,4 +244,159 @@ class InteractiveShellIntegrationTest {
} }
} }
@Suppress("UNUSED")
@StartableByRPC
class NoOpFlow : FlowLogic<Unit>() {
override val progressTracker = ProgressTracker()
override fun call() {
println("NO OP!")
}
}
@Suppress("UNUSED")
@StartableByRPC
class NoOpFlowA : FlowLogic<Unit>() {
override val progressTracker = ProgressTracker()
override fun call() {
println("NO OP! (A)")
}
}
@Suppress("UNUSED")
@StartableByRPC
class BurbleFlow : FlowLogic<Unit>() {
override val progressTracker = ProgressTracker()
override fun call() {
println("NO OP! (Burble)")
}
}
@Test
fun `shell should start flow with fully qualified class name`() {
val user = User("u", "p", setOf(all()))
var successful = false
driver(DriverParameters(notarySpecs = emptyList())) {
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
val node = nodeFuture.getOrThrow()
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
user = user.username, password = user.password,
hostAndPort = node.rpcAddress)
InteractiveShell.startShell(conf)
// setup and configure some mocks required by InteractiveShell.runFlowByNameFragment()
val output = mock<RenderPrintWriter> {
on { println(any<String>()) } doAnswer {
val line = it.arguments[0]
println("$line")
if ((line is String) && (line.startsWith("Flow completed with result:")))
successful = true
}
}
val ansiProgressRenderer = mock<ANSIProgressRenderer> {
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
}
InteractiveShell.runFlowByNameFragment(
InteractiveShellIntegrationTest::class.qualifiedName + "\$NoOpFlow",
"", output, node.rpc, ansiProgressRenderer)
}
assertThat(successful).isTrue()
}
@Test
fun `shell should start flow with unique un-qualified class name`() {
val user = User("u", "p", setOf(all()))
var successful = false
driver(DriverParameters(notarySpecs = emptyList())) {
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
val node = nodeFuture.getOrThrow()
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
user = user.username, password = user.password,
hostAndPort = node.rpcAddress)
InteractiveShell.startShell(conf)
// setup and configure some mocks required by InteractiveShell.runFlowByNameFragment()
val output = mock<RenderPrintWriter> {
on { println(any<String>()) } doAnswer {
val line = it.arguments[0]
println("$line")
if ((line is String) && (line.startsWith("Flow completed with result:")))
successful = true
}
}
val ansiProgressRenderer = mock<ANSIProgressRenderer> {
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
}
InteractiveShell.runFlowByNameFragment(
"InteractiveShellIntegrationTest\$NoOpFlowA",
"", output, node.rpc, ansiProgressRenderer)
}
assertThat(successful).isTrue()
}
@Test
fun `shell should fail to start flow with ambiguous class name`() {
val user = User("u", "p", setOf(all()))
var successful = false
driver(DriverParameters(notarySpecs = emptyList())) {
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
val node = nodeFuture.getOrThrow()
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
user = user.username, password = user.password,
hostAndPort = node.rpcAddress)
InteractiveShell.startShell(conf)
// setup and configure some mocks required by InteractiveShell.runFlowByNameFragment()
val output = mock<RenderPrintWriter> {
on { println(any<String>()) } doAnswer {
val line = it.arguments[0]
println("$line")
if ((line is String) && (line.startsWith("Ambiguous name provided, please be more specific.")))
successful = true
}
}
val ansiProgressRenderer = mock<ANSIProgressRenderer> {
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
}
InteractiveShell.runFlowByNameFragment(
InteractiveShellIntegrationTest::class.qualifiedName + "\$NoOpFlo",
"", output, node.rpc, ansiProgressRenderer)
}
assertThat(successful).isTrue()
}
@Test
fun `shell should start flow with partially matching class name`() {
val user = User("u", "p", setOf(all()))
var successful = false
driver(DriverParameters(notarySpecs = emptyList())) {
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
val node = nodeFuture.getOrThrow()
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
user = user.username, password = user.password,
hostAndPort = node.rpcAddress)
InteractiveShell.startShell(conf)
// setup and configure some mocks required by InteractiveShell.runFlowByNameFragment()
val output = mock<RenderPrintWriter> {
on { println(any<String>()) } doAnswer {
val line = it.arguments[0]
println("$line")
if ((line is String) && (line.startsWith("Flow completed with result")))
successful = true
}
}
val ansiProgressRenderer = mock<ANSIProgressRenderer> {
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
}
InteractiveShell.runFlowByNameFragment(
"Burble",
"", output, node.rpc, ansiProgressRenderer)
}
assertThat(successful).isTrue()
}
} }

View File

@ -209,6 +209,10 @@ object InteractiveShell {
// TODO: This should become the default renderer rather than something used specifically by commands. // TODO: This should become the default renderer rather than something used specifically by commands.
private val outputMapper by lazy { createOutputMapper() } private val outputMapper by lazy { createOutputMapper() }
@VisibleForTesting
lateinit var latch: CountDownLatch
private set
/** /**
* Called from the 'flow' shell command. Takes a name fragment and finds a matching flow, or prints out * Called from the 'flow' shell command. Takes a name fragment and finds a matching flow, or prints out
* the list of options if the request is ambiguous. Then parses [inputData] as constructor arguments using * the list of options if the request is ambiguous. Then parses [inputData] as constructor arguments using
@ -220,7 +224,7 @@ object InteractiveShell {
output: RenderPrintWriter, output: RenderPrintWriter,
rpcOps: CordaRPCOps, rpcOps: CordaRPCOps,
ansiProgressRenderer: ANSIProgressRenderer, ansiProgressRenderer: ANSIProgressRenderer,
om: ObjectMapper) { om: ObjectMapper = outputMapper) {
val matches = try { val matches = try {
rpcOps.registeredFlows().filter { nameFragment in it } rpcOps.registeredFlows().filter { nameFragment in it }
} catch (e: PermissionException) { } catch (e: PermissionException) {
@ -230,23 +234,24 @@ object InteractiveShell {
if (matches.isEmpty()) { if (matches.isEmpty()) {
output.println("No matching flow found, run 'flow list' to see your options.", Color.red) output.println("No matching flow found, run 'flow list' to see your options.", Color.red)
return return
} else if (matches.size > 1) { } else if (matches.size > 1 && matches.find { it.endsWith(nameFragment)} == null) {
output.println("Ambiguous name provided, please be more specific. Your options are:") output.println("Ambiguous name provided, please be more specific. Your options are:")
matches.forEachIndexed { i, s -> output.println("${i + 1}. $s", Color.yellow) } matches.forEachIndexed { i, s -> output.println("${i + 1}. $s", Color.yellow) }
return return
} }
val flowName = matches.find { it.endsWith(nameFragment)} ?: matches.single()
val flowClazz: Class<FlowLogic<*>> = if (classLoader != null) { val flowClazz: Class<FlowLogic<*>> = if (classLoader != null) {
uncheckedCast(Class.forName(matches.single(), true, classLoader)) uncheckedCast(Class.forName(flowName, true, classLoader))
} else { } else {
uncheckedCast(Class.forName(matches.single())) uncheckedCast(Class.forName(flowName))
} }
try { try {
// Show the progress tracker on the console until the flow completes or is interrupted with a // Show the progress tracker on the console until the flow completes or is interrupted with a
// Ctrl-C keypress. // Ctrl-C keypress.
val stateObservable = runFlowFromString({ clazz, args -> rpcOps.startTrackedFlowDynamic(clazz, *args) }, inputData, flowClazz, om) val stateObservable = runFlowFromString({ clazz, args -> rpcOps.startTrackedFlowDynamic(clazz, *args) }, inputData, flowClazz, om)
val latch = CountDownLatch(1) latch = CountDownLatch(1)
ansiProgressRenderer.render(stateObservable, latch::countDown) ansiProgressRenderer.render(stateObservable, latch::countDown)
// Wait for the flow to end and the progress tracker to notice. By the time the latch is released // Wait for the flow to end and the progress tracker to notice. By the time the latch is released
// the tracker is done with the screen. // the tracker is done with the screen.