Skip to content

Commit

Permalink
[CORDA-2738] Allow the ProgressTracker to cope with child trackers wi…
Browse files Browse the repository at this point in the history
…th the same steps (corda#4894)
  • Loading branch information
JamesHR3 authored and shamsasari committed Mar 18, 2019
1 parent 31100cd commit a90f394
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 87 deletions.
72 changes: 46 additions & 26 deletions core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ class ProgressTracker(vararg inputSteps: Step) {
}
}

/** The superclass of all step objects. */
/**
* The superclass of all step objects.
*/
@CordaSerializable
open class Step(open val label: String) {
open val changes: Observable<Change> get() = Observable.empty()
Expand Down Expand Up @@ -84,7 +86,9 @@ class ProgressTracker(vararg inputSteps: Step) {

private val childProgressTrackers = mutableMapOf<Step, Child>()

/** The steps in this tracker, same as the steps passed to the constructor but with UNSTARTED and DONE inserted. */
/**
* The steps in this tracker, same as the steps passed to the constructor but with UNSTARTED and DONE inserted.
*/
val steps = arrayOf(UNSTARTED, STARTING, *inputSteps, DONE)

private var _allStepsCache: List<Pair<Int, Step>> = _allSteps()
Expand All @@ -94,6 +98,10 @@ class ProgressTracker(vararg inputSteps: Step) {
private val _stepsTreeChanges by transient { ReplaySubject.create<List<Pair<Int, String>>>() }
private val _stepsTreeIndexChanges by transient { ReplaySubject.create<Int>() }

/**
* Reading returns the value of steps[stepIndex], writing moves the position of the current tracker. Once moved to
* the [DONE] state, this tracker is finished and the current step cannot be moved again.
*/
var currentStep: Step
get() = steps[stepIndex]
set(value) {
Expand Down Expand Up @@ -134,6 +142,9 @@ class ProgressTracker(vararg inputSteps: Step) {
steps.forEach {
configureChildTrackerForStep(it)
}
// Immediately update the step tree observable to ensure the first update the client receives is the initial state of the progress
// tracker.
_stepsTreeChanges.onNext(allStepsLabels)
this.currentStep = UNSTARTED
}

Expand All @@ -144,13 +155,17 @@ class ProgressTracker(vararg inputSteps: Step) {
}
}

/** The zero-based index of the current step in the [steps] array (i.e. with UNSTARTED and DONE) */
/**
* The zero-based index of the current step in the [steps] array (i.e. with UNSTARTED and DONE)
*/
var stepIndex: Int = 0
private set(value) {
field = value
}

/** The zero-bases index of the current step in a [allStepsLabels] list */
/**
* The zero-bases index of the current step in a [allStepsLabels] list
*/
var stepsTreeIndex: Int = -1
private set(value) {
if (value != field) {
Expand All @@ -160,26 +175,12 @@ class ProgressTracker(vararg inputSteps: Step) {
}

/**
* Reading returns the value of steps[stepIndex], writing moves the position of the current tracker. Once moved to
* the [DONE] state, this tracker is finished and the current step cannot be moved again.
* Returns the current step, descending into children to find the deepest step we are up to.
*/

/** Returns the current step, descending into children to find the deepest step we are up to. */
@Suppress("unused")
val currentStepRecursive: Step
get() = getChildProgressTracker(currentStep)?.currentStepRecursive ?: currentStep

/** Returns the current step, descending into children to find the deepest started step we are up to. */
private val currentStartedStepRecursive: Step
get() {
val step = getChildProgressTracker(currentStep)?.currentStartedStepRecursive ?: currentStep
return if (step == UNSTARTED) currentStep else step
}

private fun currentStepRecursiveWithoutUnstarted(): Step {
val stepRecursive = getChildProgressTracker(currentStep)?.currentStartedStepRecursive
return if (stepRecursive == null || stepRecursive == UNSTARTED) currentStep else stepRecursive
}

fun getChildProgressTracker(step: Step): ProgressTracker? = childProgressTrackers[step]?.tracker

fun setChildProgressTracker(step: ProgressTracker.Step, childProgressTracker: ProgressTracker) {
Expand Down Expand Up @@ -213,12 +214,17 @@ class ProgressTracker(vararg inputSteps: Step) {
_stepsTreeChanges.onError(error)
}

/** The parent of this tracker: set automatically by the parent when a tracker is added as a child */
/**
* The parent of this tracker: set automatically by the parent when a tracker is added as a child
*/
var parent: ProgressTracker? = null
private set

/** Walks up the tree to find the top level tracker. If this is the top level tracker, returns 'this' */
@Suppress("unused") // TODO: Review by EOY2016 if this property is useful anywhere.
/**
* Walks up the tree to find the top level tracker. If this is the top level tracker, returns 'this'.
* Required for API compatibility.
*/
@Suppress("unused")
val topLevelTracker: ProgressTracker
get() {
var cursor: ProgressTracker = this
Expand All @@ -233,9 +239,21 @@ class ProgressTracker(vararg inputSteps: Step) {
recalculateStepsTreeIndex()
}

private fun getStepIndexAtLevel(): Int {
// This gets the index of the current step in the context of this progress tracker, so it will always be at the top level in
// the allStepsCache.
val index = _allStepsCache.indexOf(Pair(0, currentStep))
return if (index >= 0) index else 0
}

private fun getCurrentStepTreeIndex(): Int {
val indexAtLevel = getStepIndexAtLevel()
val additionalIndex = getChildProgressTracker(currentStep)?.getCurrentStepTreeIndex() ?: 0
return indexAtLevel + additionalIndex
}

private fun recalculateStepsTreeIndex() {
val step = currentStepRecursiveWithoutUnstarted()
stepsTreeIndex = _allStepsCache.indexOfFirst { it.second == step }
stepsTreeIndex = getCurrentStepTreeIndex()
}

private fun _allSteps(level: Int = 0): List<Pair<Int, Step>> {
Expand Down Expand Up @@ -290,7 +308,9 @@ class ProgressTracker(vararg inputSteps: Step) {
*/
val stepsTreeIndexChanges: Observable<Int> get() = _stepsTreeIndexChanges

/** Returns true if the progress tracker has ended, either by reaching the [DONE] step or prematurely with an error */
/**
* 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()
}
// TODO: Expose the concept of errors.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ class ProgressTrackerTest {
lateinit var pt: ProgressTracker
lateinit var pt2: ProgressTracker
lateinit var pt3: ProgressTracker
lateinit var pt4: ProgressTracker

@Before
fun before() {
pt = SimpleSteps.tracker()
pt2 = ChildSteps.tracker()
pt3 = BabySteps.tracker()
pt4 = ChildSteps.tracker()
}

@Test
Expand Down Expand Up @@ -129,8 +131,8 @@ class ProgressTrackerTest {
assertCurrentStepsTree(6, SimpleSteps.THREE)

// Assert no structure changes and proper steps propagation.
assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(1, 2, 4, 6))
assertThat(stepsTreeNotification).hasSize(1) // One entry per child progress tracker set
assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(0, 1, 2, 4, 6))
assertThat(stepsTreeNotification).hasSize(2) // The initial tree state, plus one per tree update
}

@Test
Expand Down Expand Up @@ -164,8 +166,8 @@ class ProgressTrackerTest {
assertCurrentStepsTree(7, ChildSteps.SEA)

// Assert no structure changes and proper steps propagation.
assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(1, 4, 7))
assertThat(stepsTreeNotification).hasSize(2) // One entry per child progress tracker set
assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(0, 1, 4, 7))
assertThat(stepsTreeNotification).hasSize(3) // The initial tree state, plus one per update
}

@Test
Expand Down Expand Up @@ -201,8 +203,8 @@ class ProgressTrackerTest {
assertCurrentStepsTree(10, SimpleSteps.FOUR)

// Assert no structure changes and proper steps propagation.
assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(2, 7, 10))
assertThat(stepsTreeNotification).hasSize(2) // One state per child progress tracker set
assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(0, 2, 7, 10))
assertThat(stepsTreeNotification).hasSize(3) // The initial tree state, plus one per update.
}

@Test
Expand Down Expand Up @@ -236,8 +238,8 @@ class ProgressTrackerTest {
assertCurrentStepsTree(3, BabySteps.UNOS)

// Assert no structure changes and proper steps propagation.
assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(2, 5, 3))
assertThat(stepsTreeNotification).hasSize(2) // One state per child progress tracker set
assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(0, 2, 5, 3))
assertThat(stepsTreeNotification).hasSize(3) // The initial tree state, plus one per update
}

@Test
Expand All @@ -256,13 +258,13 @@ class ProgressTrackerTest {
pt.currentStep = SimpleSteps.TWO

val stepsIndexNotifications = LinkedList<Int>()
pt.stepsTreeIndexChanges.subscribe() {
pt.stepsTreeIndexChanges.subscribe {
stepsIndexNotifications += it
}

pt2.currentStep = ChildSteps.AYY

assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(1, 2, 3))
assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(0, 1, 2, 3))
}

@Test
Expand All @@ -281,20 +283,41 @@ class ProgressTrackerTest {
@Test
fun `all tree changes seen if subscribed mid flow`() {
val stepTreeNotifications = mutableListOf<List<Pair<Int, String>>>()
pt.setChildProgressTracker(SimpleSteps.TWO, pt2)
val firstStepLabels = pt.allStepsLabels

pt.currentStep = SimpleSteps.ONE
pt.currentStep = SimpleSteps.TWO
pt.setChildProgressTracker(SimpleSteps.TWO, pt2)
val secondStepLabels = pt.allStepsLabels

pt.setChildProgressTracker(SimpleSteps.TWO, pt3)
val thirdStepLabels = pt.allStepsLabels
pt.stepsTreeChanges.subscribe { stepTreeNotifications.add(it)}

fun assertStepsTree(index: Int, step: ProgressTracker.Step) {
assertEquals(step.label, stepTreeNotifications[index][pt.stepsTreeIndex].second)
}
// Should have one notification for original tree, then one for each time it changed.
assertEquals(3, stepTreeNotifications.size)
assertEquals(listOf(firstStepLabels, secondStepLabels, thirdStepLabels), stepTreeNotifications)
}

@Test
fun `trees with child trackers with duplicate steps reported correctly`() {
val stepTreeNotifications = mutableListOf<List<Pair<Int, String>>>()
val stepIndexNotifications = mutableListOf<Int>()
pt.stepsTreeChanges.subscribe { stepTreeNotifications += it }
pt.stepsTreeIndexChanges.subscribe { stepIndexNotifications += it }
pt.setChildProgressTracker(SimpleSteps.ONE, pt2)
pt.setChildProgressTracker(SimpleSteps.TWO, pt4)

pt.currentStep = SimpleSteps.ONE
pt2.currentStep = ChildSteps.AYY
pt3.currentStep = BabySteps.UNOS
assertStepsTree(0, ChildSteps.AYY)
assertStepsTree(1, BabySteps.UNOS)
pt2.nextStep()
pt2.nextStep()
pt.nextStep()
pt4.currentStep = ChildSteps.AYY

assertEquals(listOf(0, 1, 2, 3, 4, 5, 6), stepIndexNotifications)
}

@Test
fun `cannot assign step not belonging to this progress tracker`() {
assertFails { pt.currentStep = BabySteps.UNOS }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,22 @@ class CashIssueAndPaymentFlow(val amount: Amount<Currency>,
issueRef: OpaqueBytes,
recipient: Party,
anonymous: Boolean,
notary: Party) : this(amount, issueRef, recipient, anonymous, notary, ProgressTracker())
notary: Party) : this(amount, issueRef, recipient, anonymous, notary, tracker())

constructor(request: IssueAndPaymentRequest) : this(request.amount, request.issueRef, request.recipient, request.anonymous, request.notary, ProgressTracker())
constructor(request: IssueAndPaymentRequest) : this(request.amount, request.issueRef, request.recipient, request.anonymous, request.notary, tracker())

companion object {
val ISSUING_CASH = ProgressTracker.Step("Issuing cash")
val PAYING_RECIPIENT = ProgressTracker.Step("Paying recipient")

fun tracker() = ProgressTracker(ISSUING_CASH, PAYING_RECIPIENT)
}

@Suspendable
override fun call(): Result {
progressTracker.currentStep = ISSUING_CASH
subFlow(CashIssueFlow(amount, issueRef, notary))
progressTracker.currentStep = PAYING_RECIPIENT
return subFlow(CashPaymentFlow(amount, recipient, anonymous, notary))
}

Expand Down
Loading

0 comments on commit a90f394

Please sign in to comment.