mirror of
https://github.com/corda/corda.git
synced 2025-02-20 01:16:42 +00:00
ProgressTracker emits exception thrown by the flow, allowing the ANSI renderer to correctly stop and print the error (#189)
This commit is contained in:
parent
ed093cdb9d
commit
f13817efb3
@ -403,3 +403,9 @@ private class ObservableToFuture<T>(observable: Observable<T>) : AbstractFuture<
|
||||
|
||||
/** Return the sum of an Iterable of [BigDecimal]s. */
|
||||
fun Iterable<BigDecimal>.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + b }
|
||||
|
||||
fun codePointsString(vararg codePoints: Int): String {
|
||||
val builder = StringBuilder()
|
||||
codePoints.forEach { builder.append(Character.toChars(it)) }
|
||||
return builder.toString()
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.corda.core.utilities
|
||||
|
||||
import net.corda.core.codePointsString
|
||||
|
||||
/**
|
||||
* A simple wrapper class that contains icons and support for printing them only when we're connected to a terminal.
|
||||
*/
|
||||
@ -7,15 +9,17 @@ object Emoji {
|
||||
// Unfortunately only Apple has a terminal that can do colour emoji AND an emoji font installed by default.
|
||||
val hasEmojiTerminal by lazy { listOf("Apple_Terminal", "iTerm.app").contains(System.getenv("TERM_PROGRAM")) }
|
||||
|
||||
const val CODE_SANTA_CLAUS = "\ud83c\udf85"
|
||||
const val CODE_DIAMOND = "\ud83d\udd37"
|
||||
const val CODE_BAG_OF_CASH = "\ud83d\udcb0"
|
||||
const val CODE_NEWSPAPER = "\ud83d\udcf0"
|
||||
const val CODE_RIGHT_ARROW = "\u27a1\ufe0f"
|
||||
const val CODE_LEFT_ARROW = "\u2b05\ufe0f"
|
||||
const val CODE_GREEN_TICK = "\u2705"
|
||||
const val CODE_PAPERCLIP = "\ud83d\udcce"
|
||||
const val CODE_COOL_GUY = "\ud83d\ude0e"
|
||||
@JvmStatic val CODE_SANTA_CLAUS: String = codePointsString(0x1F385)
|
||||
@JvmStatic val CODE_DIAMOND: String = codePointsString(0x1F537)
|
||||
@JvmStatic val CODE_BAG_OF_CASH: String = codePointsString(0x1F4B0)
|
||||
@JvmStatic val CODE_NEWSPAPER: String = codePointsString(0x1F4F0)
|
||||
@JvmStatic val CODE_RIGHT_ARROW: String = codePointsString(0x27A1, 0xFE0F)
|
||||
@JvmStatic val CODE_LEFT_ARROW: String = codePointsString(0x2B05, 0xFE0F)
|
||||
@JvmStatic val CODE_GREEN_TICK: String = codePointsString(0x2705)
|
||||
@JvmStatic val CODE_PAPERCLIP: String = codePointsString(0x1F4CE)
|
||||
@JvmStatic val CODE_COOL_GUY: String = codePointsString(0x1F60E)
|
||||
@JvmStatic val CODE_NO_ENTRY: String = codePointsString(0x1F6AB)
|
||||
@JvmStatic val SKULL_AND_CROSSBONES: String = codePointsString(0x2620)
|
||||
|
||||
/**
|
||||
* When non-null, toString() methods are allowed to use emoji in the output as we're going to render them to a
|
||||
@ -55,4 +59,5 @@ object Emoji {
|
||||
emojiMode.set(null)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ class ProgressTracker(vararg steps: Step) {
|
||||
|
||||
/** This class makes it easier to relabel a step on the fly, to provide transient information. */
|
||||
open inner class RelabelableStep(currentLabel: String) : Step(currentLabel) {
|
||||
override val changes = BehaviorSubject.create<Change>()
|
||||
override val changes: BehaviorSubject<Change> = BehaviorSubject.create()
|
||||
|
||||
var currentLabel: String = currentLabel
|
||||
set(value) {
|
||||
@ -105,27 +105,26 @@ class ProgressTracker(vararg steps: Step) {
|
||||
var currentStep: Step
|
||||
get() = steps[stepIndex]
|
||||
set(value) {
|
||||
if (currentStep != value) {
|
||||
check(currentStep != DONE) { "Cannot rewind a progress tracker once it reaches the done state" }
|
||||
check(!hasEnded) { "Cannot rewind a progress tracker once it has ended" }
|
||||
if (currentStep == value) return
|
||||
|
||||
val index = steps.indexOf(value)
|
||||
require(index != -1)
|
||||
val index = steps.indexOf(value)
|
||||
require(index != -1)
|
||||
|
||||
if (index < stepIndex) {
|
||||
// We are going backwards: unlink and unsubscribe from any child nodes that we're rolling back
|
||||
// through, in preparation for moving through them again.
|
||||
for (i in stepIndex downTo index) {
|
||||
removeChildProgressTracker(steps[i])
|
||||
}
|
||||
if (index < stepIndex) {
|
||||
// We are going backwards: unlink and unsubscribe from any child nodes that we're rolling back
|
||||
// through, in preparation for moving through them again.
|
||||
for (i in stepIndex downTo index) {
|
||||
removeChildProgressTracker(steps[i])
|
||||
}
|
||||
|
||||
curChangeSubscription?.unsubscribe()
|
||||
stepIndex = index
|
||||
_changes.onNext(Change.Position(this, steps[index]))
|
||||
curChangeSubscription = currentStep.changes.subscribe { _changes.onNext(it) }
|
||||
|
||||
if (currentStep == DONE) _changes.onCompleted()
|
||||
}
|
||||
|
||||
curChangeSubscription?.unsubscribe()
|
||||
stepIndex = index
|
||||
_changes.onNext(Change.Position(this, steps[index]))
|
||||
curChangeSubscription = currentStep.changes.subscribe( { _changes.onNext(it) }, { _changes.onError(it) })
|
||||
|
||||
if (currentStep == DONE) _changes.onCompleted()
|
||||
}
|
||||
|
||||
/** Returns the current step, descending into children to find the deepest step we are up to. */
|
||||
@ -149,6 +148,15 @@ class ProgressTracker(vararg steps: Step) {
|
||||
_changes.onNext(Change.Structural(this, step))
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the progress tracker with the given error, bypassing any remaining steps. [changes] will emit the exception
|
||||
* as an error.
|
||||
*/
|
||||
fun endWithError(error: Throwable) {
|
||||
check(!hasEnded) { "Progress tracker has already ended" }
|
||||
_changes.onError(error)
|
||||
}
|
||||
|
||||
/** The parent of this tracker: set automatically by the parent when a tracker is added as a child */
|
||||
var parent: ProgressTracker? = null
|
||||
private set
|
||||
@ -195,6 +203,9 @@ class ProgressTracker(vararg steps: Step) {
|
||||
* if a step changed its label or rendering).
|
||||
*/
|
||||
val changes: Observable<Change> get() = _changes
|
||||
|
||||
/** Returns true if the progress tracker has ended, either by reaching the [DONE] step or prematurely with an error */
|
||||
val hasEnded: Boolean get() = _changes.hasCompleted() || _changes.hasThrowable()
|
||||
}
|
||||
|
||||
|
||||
|
@ -15,6 +15,7 @@ import net.corda.core.flows.FlowStateMachine
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.random63BitValue
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.trace
|
||||
@ -56,8 +57,8 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
|
||||
@Transient private var _logger: Logger? = null
|
||||
/**
|
||||
* Return the logger for this state machine. The logger name incorporates [id] and so including this in the log
|
||||
* message is not necessary.
|
||||
* Return the logger for this state machine. The logger name incorporates [id] and so including it in the log message
|
||||
* is not necessary.
|
||||
*/
|
||||
override val logger: Logger get() {
|
||||
return _logger ?: run {
|
||||
@ -94,14 +95,12 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
} catch (e: FlowException) {
|
||||
// Check if the FlowException was propagated by looking at where the stack trace originates (see suspendAndExpectReceive).
|
||||
val propagated = e.stackTrace[0].className == javaClass.name
|
||||
actionOnEnd(e, propagated)
|
||||
_resultFuture?.setException(e)
|
||||
processException(e, propagated)
|
||||
logger.debug(if (propagated) "Flow ended due to receiving exception" else "Flow finished with exception", e)
|
||||
return
|
||||
} catch (t: Throwable) {
|
||||
logger.warn("Terminated by unexpected exception", t)
|
||||
actionOnEnd(t, false)
|
||||
_resultFuture?.setException(t)
|
||||
processException(t, false)
|
||||
return
|
||||
}
|
||||
|
||||
@ -112,6 +111,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
// This is to prevent actionOnEnd being called twice if it throws an exception
|
||||
actionOnEnd(null, false)
|
||||
_resultFuture?.set(result)
|
||||
logic.progressTracker?.currentStep = ProgressTracker.DONE
|
||||
logger.debug { "Flow finished with result $result" }
|
||||
}
|
||||
|
||||
@ -121,6 +121,12 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
logger.trace { "Starting database transaction ${TransactionManager.currentOrNull()} on ${Strand.currentStrand()}" }
|
||||
}
|
||||
|
||||
private fun processException(exception: Throwable, propagated: Boolean) {
|
||||
actionOnEnd(exception, propagated)
|
||||
_resultFuture?.setException(exception)
|
||||
logic.progressTracker?.endWithError(exception)
|
||||
}
|
||||
|
||||
internal fun commitTransaction() {
|
||||
val transaction = TransactionManager.current()
|
||||
try {
|
||||
|
@ -24,7 +24,6 @@ import net.corda.core.messaging.send
|
||||
import net.corda.core.random63BitValue
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.then
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.trace
|
||||
@ -391,7 +390,6 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
}
|
||||
fiber.actionOnEnd = { exception, propagated ->
|
||||
try {
|
||||
fiber.logic.progressTracker?.currentStep = ProgressTracker.DONE
|
||||
mutex.locked {
|
||||
stateMachines.remove(fiber)?.let { checkpointStorage.removeCheckpoint(it) }
|
||||
notifyChangeObservers(fiber, AddOrRemove.REMOVE)
|
||||
|
@ -2,7 +2,6 @@ package net.corda.node.utilities
|
||||
|
||||
import net.corda.core.ThreadBox
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
import java.util.*
|
||||
|
||||
@ -35,13 +34,12 @@ class ANSIProgressObserver(val smm: StateMachineManager) {
|
||||
if (currentlyRendering?.progressTracker != null) {
|
||||
ANSIProgressRenderer.progressTracker = currentlyRendering!!.progressTracker
|
||||
}
|
||||
} while (currentlyRendering?.progressTracker?.currentStep == ProgressTracker.DONE)
|
||||
} while (currentlyRendering?.progressTracker?.hasEnded ?: false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeFlowLogic(flowLogic: FlowLogic<*>) {
|
||||
state.locked {
|
||||
flowLogic.progressTracker?.currentStep = ProgressTracker.DONE
|
||||
if (currentlyRendering == flowLogic) {
|
||||
wireUpProgressRendering()
|
||||
}
|
||||
@ -51,7 +49,7 @@ class ANSIProgressObserver(val smm: StateMachineManager) {
|
||||
private fun addFlowLogic(flowLogic: FlowLogic<*>) {
|
||||
state.locked {
|
||||
pending.add(flowLogic)
|
||||
if ((currentlyRendering?.progressTracker?.currentStep ?: ProgressTracker.DONE) == ProgressTracker.DONE) {
|
||||
if (currentlyRendering?.progressTracker?.hasEnded ?: true) {
|
||||
wireUpProgressRendering()
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package net.corda.node.utilities
|
||||
|
||||
import net.corda.core.utilities.Emoji
|
||||
import net.corda.core.utilities.Emoji.CODE_GREEN_TICK
|
||||
import net.corda.core.utilities.Emoji.CODE_NO_ENTRY
|
||||
import net.corda.core.utilities.Emoji.CODE_RIGHT_ARROW
|
||||
import net.corda.core.utilities.Emoji.SKULL_AND_CROSSBONES
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.node.utilities.ANSIProgressRenderer.progressTracker
|
||||
import org.apache.logging.log4j.LogManager
|
||||
@ -43,7 +46,7 @@ object ANSIProgressRenderer {
|
||||
prevMessagePrinted = null
|
||||
prevLinesDrawn = 0
|
||||
draw(true)
|
||||
subscription = value?.changes?.subscribe { draw(true) }
|
||||
subscription = value?.changes?.subscribe({ draw(true) }, { draw(true, it) })
|
||||
}
|
||||
|
||||
private fun setup() {
|
||||
@ -102,7 +105,7 @@ object ANSIProgressRenderer {
|
||||
// prevLinesDraw is just for ANSI mode.
|
||||
private var prevLinesDrawn = 0
|
||||
|
||||
@Synchronized private fun draw(moveUp: Boolean) {
|
||||
@Synchronized private fun draw(moveUp: Boolean, error: Throwable? = null) {
|
||||
val pt = progressTracker!!
|
||||
|
||||
if (!usingANSI) {
|
||||
@ -122,7 +125,15 @@ object ANSIProgressRenderer {
|
||||
// Put a blank line between any logging and us.
|
||||
ansi.eraseLine()
|
||||
ansi.newline()
|
||||
val newLinesDrawn = 1 + pt.renderLevel(ansi, 0, pt.allSteps)
|
||||
var newLinesDrawn = 1 + pt.renderLevel(ansi, 0, error != null)
|
||||
|
||||
if (error != null) {
|
||||
ansi.a("$SKULL_AND_CROSSBONES $error")
|
||||
ansi.eraseLine(Ansi.Erase.FORWARD)
|
||||
ansi.newline()
|
||||
newLinesDrawn++
|
||||
}
|
||||
|
||||
if (newLinesDrawn < prevLinesDrawn) {
|
||||
// If some steps were removed from the progress tracker, we don't want to leave junk hanging around below.
|
||||
val linesToClear = prevLinesDrawn - newLinesDrawn
|
||||
@ -140,7 +151,7 @@ object ANSIProgressRenderer {
|
||||
}
|
||||
|
||||
// Returns number of lines rendered.
|
||||
private fun ProgressTracker.renderLevel(ansi: Ansi, indent: Int, allSteps: List<Pair<Int, ProgressTracker.Step>>): Int {
|
||||
private fun ProgressTracker.renderLevel(ansi: Ansi, indent: Int, error: Boolean): Int {
|
||||
with(ansi) {
|
||||
var lines = 0
|
||||
for ((index, step) in steps.withIndex()) {
|
||||
@ -149,10 +160,11 @@ object ANSIProgressRenderer {
|
||||
if (indent > 0 && step == ProgressTracker.DONE) continue
|
||||
|
||||
val marker = when {
|
||||
index < stepIndex -> Emoji.CODE_GREEN_TICK + " "
|
||||
index == stepIndex && step == ProgressTracker.DONE -> Emoji.CODE_GREEN_TICK + " "
|
||||
index == stepIndex -> Emoji.CODE_RIGHT_ARROW + " "
|
||||
else -> " "
|
||||
index < stepIndex -> "$CODE_GREEN_TICK "
|
||||
index == stepIndex && step == ProgressTracker.DONE -> "$CODE_GREEN_TICK "
|
||||
index == stepIndex -> "$CODE_RIGHT_ARROW "
|
||||
error -> "$CODE_NO_ENTRY "
|
||||
else -> " "
|
||||
}
|
||||
a(" ".repeat(indent))
|
||||
a(marker)
|
||||
@ -168,7 +180,7 @@ object ANSIProgressRenderer {
|
||||
|
||||
val child = getChildProgressTracker(step)
|
||||
if (child != null)
|
||||
lines += child.renderLevel(ansi, indent + 1, allSteps)
|
||||
lines += child.renderLevel(ansi, indent + 1, error)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package net.corda.node.services.statemachine
|
||||
import co.paralleluniverse.fibers.Fiber
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import net.corda.core.*
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.contracts.DummyState
|
||||
import net.corda.core.contracts.issuedBy
|
||||
@ -10,17 +11,16 @@ import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.map
|
||||
import net.corda.core.messaging.MessageRecipients
|
||||
import net.corda.core.node.services.PartyInfo
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.random63BitValue
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.LogHelper
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.ProgressTracker.Change
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
@ -44,6 +44,7 @@ import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import rx.Notification
|
||||
import rx.Observable
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
@ -379,18 +380,36 @@ class StateMachineManagerTests {
|
||||
net.runNetwork()
|
||||
assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy {
|
||||
resultFuture.getOrThrow()
|
||||
}.withMessageContaining(String::class.java.name)
|
||||
}.withMessageContaining(String::class.java.name) // Make sure the exception message mentions the type the flow was expecting to receive
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non-FlowException thrown on other side`() {
|
||||
node2.services.registerFlowInitiator(ReceiveFlow::class) { ExceptionFlow { Exception("evil bug!") } }
|
||||
val resultFuture = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture
|
||||
net.runNetwork()
|
||||
val exceptionResult = assertFailsWith(FlowSessionException::class) {
|
||||
resultFuture.getOrThrow()
|
||||
val erroringFlowFuture = node2.initiateSingleShotFlow(ReceiveFlow::class) {
|
||||
ExceptionFlow { Exception("evil bug!") }
|
||||
}
|
||||
assertThat(exceptionResult.message).doesNotContain("evil bug!")
|
||||
val erroringFlowSteps = erroringFlowFuture.flatMap { it.progressSteps }
|
||||
|
||||
val receiveFlow = ReceiveFlow(node2.info.legalIdentity)
|
||||
val receiveFlowSteps = receiveFlow.progressSteps
|
||||
val receiveFlowResult = node1.services.startFlow(receiveFlow).resultFuture
|
||||
|
||||
net.runNetwork()
|
||||
|
||||
assertThat(erroringFlowSteps.get()).containsExactly(
|
||||
Notification.createOnNext(ExceptionFlow.START_STEP),
|
||||
Notification.createOnError(erroringFlowFuture.get().exceptionThrown)
|
||||
)
|
||||
|
||||
val receiveFlowException = assertFailsWith(FlowSessionException::class) {
|
||||
receiveFlowResult.getOrThrow()
|
||||
}
|
||||
assertThat(receiveFlowException.message).doesNotContain("evil bug!")
|
||||
assertThat(receiveFlowSteps.get()).containsExactly(
|
||||
Notification.createOnNext(ReceiveFlow.START_STEP),
|
||||
Notification.createOnError(receiveFlowException)
|
||||
)
|
||||
|
||||
assertSessionTransfers(
|
||||
node1 sent sessionInit(ReceiveFlow::class) to node2,
|
||||
node2 sent sessionConfirm to node1,
|
||||
@ -400,11 +419,15 @@ class StateMachineManagerTests {
|
||||
|
||||
@Test
|
||||
fun `FlowException thrown on other side`() {
|
||||
val erroringFlowFuture = node2.initiateSingleShotFlow(ReceiveFlow::class) {
|
||||
val erroringFlow = node2.initiateSingleShotFlow(ReceiveFlow::class) {
|
||||
ExceptionFlow { MyFlowException("Nothing useful") }
|
||||
}
|
||||
val erroringFlowSteps = erroringFlow.flatMap { it.progressSteps }
|
||||
|
||||
val receivingFiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)) as FlowStateMachineImpl
|
||||
|
||||
net.runNetwork()
|
||||
|
||||
assertThatExceptionOfType(MyFlowException::class.java)
|
||||
.isThrownBy { receivingFiber.resultFuture.getOrThrow() }
|
||||
.withMessage("Nothing useful")
|
||||
@ -412,13 +435,18 @@ class StateMachineManagerTests {
|
||||
databaseTransaction(node2.database) {
|
||||
assertThat(node2.checkpointStorage.checkpoints()).isEmpty()
|
||||
}
|
||||
val errorFlow = erroringFlowFuture.getOrThrow()
|
||||
|
||||
assertThat(receivingFiber.isTerminated).isTrue()
|
||||
assertThat((errorFlow.stateMachine as FlowStateMachineImpl).isTerminated).isTrue()
|
||||
assertThat((erroringFlow.get().stateMachine as FlowStateMachineImpl).isTerminated).isTrue()
|
||||
assertThat(erroringFlowSteps.get()).containsExactly(
|
||||
Notification.createOnNext(ExceptionFlow.START_STEP),
|
||||
Notification.createOnError(erroringFlow.get().exceptionThrown)
|
||||
)
|
||||
|
||||
assertSessionTransfers(
|
||||
node1 sent sessionInit(ReceiveFlow::class) to node2,
|
||||
node2 sent sessionConfirm to node1,
|
||||
node2 sent erroredEnd(errorFlow.exceptionThrown) to node1
|
||||
node2 sent erroredEnd(erroringFlow.get().exceptionThrown) to node1
|
||||
)
|
||||
// Make sure the original stack trace isn't sent down the wire
|
||||
assertThat((sessionTransfers.last().message as ErrorSessionEnd).errorResponse!!.stackTrace).isEmpty()
|
||||
@ -606,6 +634,15 @@ class StateMachineManagerTests {
|
||||
private infix fun MockNode.sent(message: SessionMessage): Pair<Int, SessionMessage> = Pair(id, message)
|
||||
private infix fun Pair<Int, SessionMessage>.to(node: MockNode): SessionTransfer = SessionTransfer(first, second, node.net.myAddress)
|
||||
|
||||
private val FlowLogic<*>.progressSteps: ListenableFuture<List<Notification<ProgressTracker.Step>>> get() {
|
||||
return progressTracker!!.changes
|
||||
.ofType(Change.Position::class.java)
|
||||
.map { it.newStep }
|
||||
.materialize()
|
||||
.toList()
|
||||
.toFuture()
|
||||
}
|
||||
|
||||
private class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic<Unit>() {
|
||||
@Transient var flowStarted = false
|
||||
|
||||
@ -630,17 +667,22 @@ class StateMachineManagerTests {
|
||||
|
||||
|
||||
private class ReceiveFlow(vararg val otherParties: Party) : FlowLogic<Unit>() {
|
||||
private var nonTerminating: Boolean = false
|
||||
object START_STEP : ProgressTracker.Step("Starting")
|
||||
object RECEIVED_STEP : ProgressTracker.Step("Received")
|
||||
|
||||
init {
|
||||
require(otherParties.isNotEmpty())
|
||||
}
|
||||
|
||||
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP, RECEIVED_STEP)
|
||||
private var nonTerminating: Boolean = false
|
||||
@Transient var receivedPayloads: List<String> = emptyList()
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
progressTracker.currentStep = START_STEP
|
||||
receivedPayloads = otherParties.map { receive<String>(it).unwrap { it } }
|
||||
progressTracker.currentStep = RECEIVED_STEP
|
||||
if (nonTerminating) {
|
||||
Fiber.park()
|
||||
}
|
||||
@ -664,8 +706,13 @@ class StateMachineManagerTests {
|
||||
}
|
||||
|
||||
private class ExceptionFlow<E : Exception>(val exception: () -> E) : FlowLogic<Nothing>() {
|
||||
object START_STEP : ProgressTracker.Step("Starting")
|
||||
|
||||
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP)
|
||||
lateinit var exceptionThrown: E
|
||||
|
||||
override fun call(): Nothing {
|
||||
progressTracker.currentStep = START_STEP
|
||||
exceptionThrown = exception()
|
||||
throw exceptionThrown
|
||||
}
|
||||
|
@ -4,31 +4,34 @@ import com.google.common.util.concurrent.Futures
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.flows.IssuerFlow
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.messaging.CordaRPCClient
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.testing.node.NodeBasedTest
|
||||
import org.junit.Test
|
||||
|
||||
class TraderDemoTest {
|
||||
@Test fun `runs trader demo`() {
|
||||
driver(isDebug = true) {
|
||||
val permissions = setOf(
|
||||
startFlowPermission<IssuerFlow.IssuanceRequester>(),
|
||||
startFlowPermission<net.corda.traderdemo.flow.SellerFlow>())
|
||||
val demoUser = listOf(User("demo", "demo", permissions))
|
||||
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||
val (nodeA, nodeB) = Futures.allAsList(
|
||||
startNode("Bank A", rpcUsers = demoUser),
|
||||
startNode("Bank B", rpcUsers = demoUser),
|
||||
startNode("BankOfCorda", rpcUsers = listOf(user)),
|
||||
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
).getOrThrow()
|
||||
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB)
|
||||
.map { it.rpcClientToNode().start(demoUser[0].username, demoUser[0].password).proxy() }
|
||||
class TraderDemoTest : NodeBasedTest() {
|
||||
@Test
|
||||
fun `runs trader demo`() {
|
||||
val permissions = setOf(
|
||||
startFlowPermission<IssuerFlow.IssuanceRequester>(),
|
||||
startFlowPermission<net.corda.traderdemo.flow.SellerFlow>())
|
||||
val demoUser = listOf(User("demo", "demo", permissions))
|
||||
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||
val (nodeA, nodeB) = Futures.allAsList(
|
||||
startNode("Bank A", rpcUsers = demoUser),
|
||||
startNode("Bank B", rpcUsers = demoUser),
|
||||
startNode("BankOfCorda", rpcUsers = listOf(user)),
|
||||
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
).getOrThrow()
|
||||
|
||||
assert(TraderDemoClientApi(nodeARpc).runBuyer())
|
||||
assert(TraderDemoClientApi(nodeBRpc).runSeller(counterparty = nodeA.nodeInfo.legalIdentity.name))
|
||||
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map {
|
||||
val client = CordaRPCClient(it.configuration.artemisAddress, it.configuration)
|
||||
client.start(demoUser[0].username, demoUser[0].password).proxy()
|
||||
}
|
||||
|
||||
TraderDemoClientApi(nodeARpc).runBuyer()
|
||||
TraderDemoClientApi(nodeBRpc).runSeller(counterparty = nodeA.info.legalIdentity.name)
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) {
|
||||
val logger = loggerFor<TraderDemoClientApi>()
|
||||
}
|
||||
|
||||
fun runBuyer(amount: Amount<Currency> = 30000.0.DOLLARS): Boolean {
|
||||
fun runBuyer(amount: Amount<Currency> = 30000.0.DOLLARS) {
|
||||
val bankOfCordaParty = rpc.partyFromName(BOC.name)
|
||||
?: throw Exception("Unable to locate ${BOC.name} in Network Map Service")
|
||||
val me = rpc.nodeIdentity()
|
||||
@ -35,31 +35,25 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) {
|
||||
}
|
||||
|
||||
Futures.allAsList(resultFutures).getOrThrow()
|
||||
return true
|
||||
}
|
||||
|
||||
fun runSeller(amount: Amount<Currency> = 1000.0.DOLLARS, counterparty: String): Boolean {
|
||||
val otherParty = rpc.partyFromName(counterparty)
|
||||
if (otherParty != null) {
|
||||
// The seller will sell some commercial paper to the buyer, who will pay with (self issued) cash.
|
||||
//
|
||||
// The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an
|
||||
// attachment. Make sure we have the transaction prospectus attachment loaded into our store.
|
||||
//
|
||||
// This can also be done via an HTTP upload, but here we short-circuit and do it from code.
|
||||
if (!rpc.attachmentExists(SellerFlow.PROSPECTUS_HASH)) {
|
||||
javaClass.classLoader.getResourceAsStream("bank-of-london-cp.jar").use {
|
||||
val id = rpc.uploadAttachment(it)
|
||||
assertEquals(SellerFlow.PROSPECTUS_HASH, id)
|
||||
}
|
||||
fun runSeller(amount: Amount<Currency> = 1000.0.DOLLARS, counterparty: String) {
|
||||
val otherParty = rpc.partyFromName(counterparty) ?: throw IllegalStateException("Don't know $counterparty")
|
||||
// The seller will sell some commercial paper to the buyer, who will pay with (self issued) cash.
|
||||
//
|
||||
// The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an
|
||||
// attachment. Make sure we have the transaction prospectus attachment loaded into our store.
|
||||
//
|
||||
// This can also be done via an HTTP upload, but here we short-circuit and do it from code.
|
||||
if (!rpc.attachmentExists(SellerFlow.PROSPECTUS_HASH)) {
|
||||
javaClass.classLoader.getResourceAsStream("bank-of-london-cp.jar").use {
|
||||
val id = rpc.uploadAttachment(it)
|
||||
assertEquals(SellerFlow.PROSPECTUS_HASH, id)
|
||||
}
|
||||
|
||||
// The line below blocks and waits for the future to resolve.
|
||||
val stx = rpc.startFlow(::SellerFlow, otherParty, amount).returnValue.getOrThrow()
|
||||
logger.info("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(stx.tx)}")
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
// The line below blocks and waits for the future to resolve.
|
||||
val stx = rpc.startFlow(::SellerFlow, otherParty, amount).returnValue.getOrThrow()
|
||||
logger.info("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(stx.tx)}")
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user