mirror of
https://github.com/corda/corda.git
synced 2025-06-21 16:49:45 +00:00
[CORDA-2738] Allow the ProgressTracker to cope with child trackers with the same steps (#4894)
This commit is contained in:
@ -16,22 +16,23 @@ import org.fusesource.jansi.Ansi
|
||||
import org.fusesource.jansi.Ansi.Attribute
|
||||
import org.fusesource.jansi.AnsiConsole
|
||||
import org.fusesource.jansi.AnsiOutputStream
|
||||
import rx.Observable.combineLatest
|
||||
import rx.Subscription
|
||||
import java.util.*
|
||||
import java.util.stream.IntStream
|
||||
import kotlin.streams.toList
|
||||
|
||||
abstract class ANSIProgressRenderer {
|
||||
|
||||
private var subscriptionIndex: Subscription? = null
|
||||
private var subscriptionTree: Subscription? = null
|
||||
private var updatesSubscription: Subscription? = null
|
||||
|
||||
protected var usingANSI = false
|
||||
protected var checkEmoji = false
|
||||
private val usingUnicode = !SystemUtils.IS_OS_WINDOWS
|
||||
|
||||
protected var treeIndex: Int = 0
|
||||
protected var treeIndexProcessed: MutableSet<Int> = mutableSetOf()
|
||||
protected var tree: List<Pair<Int,String>> = listOf()
|
||||
private var treeIndex: Int = 0
|
||||
private var treeIndexProcessed: MutableSet<Int> = mutableSetOf()
|
||||
protected var tree: List<ProgressStep> = listOf()
|
||||
|
||||
private var installedYet = false
|
||||
|
||||
@ -42,15 +43,18 @@ abstract class ANSIProgressRenderer {
|
||||
// prevLinesDraw is just for ANSI mode.
|
||||
protected var prevLinesDrawn = 0
|
||||
|
||||
data class ProgressStep(val level: Int, val description: String, val parentIndex: Int?)
|
||||
data class InputTreeStep(val level: Int, val description: String)
|
||||
|
||||
private fun done(error: Throwable?) {
|
||||
if (error == null) _render(null)
|
||||
if (error == null) renderInternal(null)
|
||||
draw(true, error)
|
||||
onDone()
|
||||
}
|
||||
|
||||
fun render(flowProgressHandle: FlowProgressHandle<*>, onDone: () -> Unit = {}) {
|
||||
this.onDone = onDone
|
||||
_render(flowProgressHandle)
|
||||
renderInternal(flowProgressHandle)
|
||||
}
|
||||
|
||||
protected abstract fun printLine(line:String)
|
||||
@ -59,9 +63,8 @@ abstract class ANSIProgressRenderer {
|
||||
|
||||
protected abstract fun setup()
|
||||
|
||||
private fun _render(flowProgressHandle: FlowProgressHandle<*>?) {
|
||||
subscriptionIndex?.unsubscribe()
|
||||
subscriptionTree?.unsubscribe()
|
||||
private fun renderInternal(flowProgressHandle: FlowProgressHandle<*>?) {
|
||||
updatesSubscription?.unsubscribe()
|
||||
treeIndex = 0
|
||||
treeIndexProcessed.clear()
|
||||
tree = listOf()
|
||||
@ -75,27 +78,64 @@ abstract class ANSIProgressRenderer {
|
||||
prevLinesDrawn = 0
|
||||
draw(true)
|
||||
|
||||
val treeUpdates = flowProgressHandle?.stepsTreeFeed?.updates
|
||||
val indexUpdates = flowProgressHandle?.stepsTreeIndexFeed?.updates
|
||||
|
||||
flowProgressHandle?.apply {
|
||||
stepsTreeIndexFeed?.apply {
|
||||
treeIndexProcessed.add(snapshot)
|
||||
subscriptionIndex = updates.subscribe({
|
||||
treeIndex = it
|
||||
treeIndexProcessed.add(it)
|
||||
if (treeUpdates == null || indexUpdates == null) {
|
||||
renderInBold("Cannot print progress for this flow as the required data is missing", Ansi())
|
||||
} else {
|
||||
// By combining the two observables, a race condition where both emit items at roughly the same time is avoided. This could
|
||||
// result in steps being incorrectly marked as skipped. Instead, whenever either observable emits an item, a pair of the
|
||||
// last index and last tree is returned, which ensures that updates to either are processed in series.
|
||||
updatesSubscription = combineLatest(treeUpdates, indexUpdates) { tree, index -> Pair(tree, index) }.subscribe(
|
||||
{
|
||||
val newTree = transformTree(it.first.map { elem -> InputTreeStep(elem.first, elem.second) })
|
||||
// Process indices first, as if the tree has changed the associated index with this update is for the old tree. Note
|
||||
// that the one case where this isn't true is the very first update, but in this case the index should be 0 (as this
|
||||
// update is for the initial state). The remapping on a new tree assumes the step at index 0 is always at least current,
|
||||
// so this case is handled there.
|
||||
treeIndex = it.second
|
||||
treeIndexProcessed.add(it.second)
|
||||
if (newTree != tree) {
|
||||
remapIndices(newTree)
|
||||
tree = newTree
|
||||
}
|
||||
draw(true)
|
||||
}, { done(it) }, { done(null) })
|
||||
}
|
||||
stepsTreeFeed?.apply {
|
||||
subscriptionTree = updates.subscribe({
|
||||
remapIndices(it)
|
||||
tree = it
|
||||
draw(true)
|
||||
}, { done(it) }, { done(null) })
|
||||
}
|
||||
},
|
||||
{ done(it) },
|
||||
{ done(null) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun remapIndices(newTree: List<Pair<Int, String>>) {
|
||||
// Create a new tree of steps that also holds a reference to the parent of each step. This is required to uniquely identify each step
|
||||
// (assuming that each step label is unique at a given level).
|
||||
private fun transformTree(inputTree: List<InputTreeStep>): List<ProgressStep> {
|
||||
if (inputTree.isEmpty()) {
|
||||
return listOf()
|
||||
}
|
||||
val stack = Stack<Pair<Int, InputTreeStep>>()
|
||||
stack.push(Pair(0, inputTree[0]))
|
||||
return inputTree.mapIndexed { index, step ->
|
||||
val parentIndex = try {
|
||||
val top = stack.peek()
|
||||
val levelDifference = top.second.level - step.level
|
||||
if (levelDifference >= 0) {
|
||||
// The top of the stack is at the same or lower level than the current step. Remove items from the top until the topmost
|
||||
// item is at a higher level - this is the parent step.
|
||||
repeat(levelDifference + 1) { stack.pop() }
|
||||
}
|
||||
stack.peek().first
|
||||
} catch (e: EmptyStackException) {
|
||||
// If there is nothing on the stack at any point, it implies that this step is at the top level and has no parent.
|
||||
null
|
||||
}
|
||||
stack.push(Pair(index, step))
|
||||
ProgressStep(step.level, step.description, parentIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private fun remapIndices(newTree: List<ProgressStep>) {
|
||||
val newIndices = newTree.filter {
|
||||
treeIndexProcessed.contains(tree.indexOf(it))
|
||||
}.map {
|
||||
@ -108,7 +148,7 @@ abstract class ANSIProgressRenderer {
|
||||
@Synchronized protected fun draw(moveUp: Boolean, error: Throwable? = null) {
|
||||
|
||||
if (!usingANSI) {
|
||||
val currentMessage = tree.getOrNull(treeIndex)?.second
|
||||
val currentMessage = tree.getOrNull(treeIndex)?.description
|
||||
if (currentMessage != null && currentMessage != prevMessagePrinted) {
|
||||
printLine(currentMessage)
|
||||
prevMessagePrinted = currentMessage
|
||||
@ -182,13 +222,13 @@ abstract class ANSIProgressRenderer {
|
||||
error -> if (usingUnicode) "${Emoji.noEntry} " else "ERROR: "
|
||||
else -> " " // Not reached yet.
|
||||
}
|
||||
a(" ".repeat(step.first))
|
||||
a(" ".repeat(step.level))
|
||||
a(marker)
|
||||
|
||||
when {
|
||||
activeStep -> renderInBold(step.second, ansi)
|
||||
skippedStep -> renderInFaint(step.second, ansi)
|
||||
else -> a(step.second)
|
||||
activeStep -> renderInBold(step.description, ansi)
|
||||
skippedStep -> renderInFaint(step.description, ansi)
|
||||
else -> a(step.description)
|
||||
}
|
||||
|
||||
eraseLine(Ansi.Erase.FORWARD)
|
||||
|
@ -40,6 +40,10 @@ class ANSIProgressRendererTest {
|
||||
fun stepActive(stepLabel: String): String {
|
||||
return if (SystemUtils.IS_OS_WINDOWS) """CURRENT: $INTENSITY_BOLD_ON_ASCII$stepLabel$INTENSITY_OFF_ASCII""" else """▶︎ $INTENSITY_BOLD_ON_ASCII$stepLabel$INTENSITY_OFF_ASCII"""
|
||||
}
|
||||
|
||||
fun stepNotRun(stepLabel: String): String {
|
||||
return """ $stepLabel"""
|
||||
}
|
||||
}
|
||||
|
||||
lateinit var printWriter: RenderPrintWriter
|
||||
@ -59,35 +63,57 @@ class ANSIProgressRendererTest {
|
||||
flowProgressHandle = FlowProgressHandleImpl(StateMachineRunId.createRandom(), openFuture<String>(), Observable.empty(), stepsTreeIndexFeed, stepsTreeFeed)
|
||||
}
|
||||
|
||||
private fun checkTrackingState(captor: KArgumentCaptor<Ansi>, updates: Int, trackerState: List<String>) {
|
||||
verify(printWriter, times(updates)).print(captor.capture())
|
||||
assertThat(captor.lastValue.toString()).containsSequence(trackerState)
|
||||
verify(printWriter, times(updates)).flush()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that steps are rendered appropriately depending on their status`() {
|
||||
progressRenderer.render(flowProgressHandle)
|
||||
feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(0, STEP_2_LABEL), Pair(0, STEP_3_LABEL)))
|
||||
// The flow is currently at step 3, while step 1 has been completed and step 2 has been skipped.
|
||||
indexSubject.onNext(0)
|
||||
indexSubject.onNext(2)
|
||||
|
||||
val captor = argumentCaptor<Ansi>()
|
||||
verify(printWriter, times(2)).print(captor.capture())
|
||||
assertThat(captor.secondValue.toString()).containsSequence(stepSuccess(STEP_1_LABEL), stepSkipped(STEP_2_LABEL), stepActive(STEP_3_LABEL))
|
||||
verify(printWriter, times(2)).flush()
|
||||
checkTrackingState(captor, 2, listOf(stepSuccess(STEP_1_LABEL), stepSkipped(STEP_2_LABEL), stepActive(STEP_3_LABEL)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changing tree causes correct steps to be marked as done`() {
|
||||
progressRenderer.render(flowProgressHandle)
|
||||
feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(1, STEP_2_LABEL), Pair(1, STEP_3_LABEL), Pair(0, STEP_4_LABEL), Pair(0, STEP_5_LABEL)))
|
||||
indexSubject.onNext(0)
|
||||
indexSubject.onNext(1)
|
||||
indexSubject.onNext(2)
|
||||
|
||||
val captor = argumentCaptor<Ansi>()
|
||||
verify(printWriter, times(3)).print(captor.capture())
|
||||
assertThat(captor.lastValue.toString()).containsSequence(stepSuccess(STEP_1_LABEL), stepSuccess(STEP_2_LABEL), stepActive(STEP_3_LABEL))
|
||||
verify(printWriter, times(3)).flush()
|
||||
checkTrackingState(captor, 3, listOf(stepSuccess(STEP_1_LABEL), stepSuccess(STEP_2_LABEL), stepActive(STEP_3_LABEL)))
|
||||
|
||||
feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(0, STEP_4_LABEL), Pair(0, STEP_5_LABEL)))
|
||||
verify(printWriter, times(4)).print(captor.capture())
|
||||
assertThat(captor.lastValue.toString()).containsSequence(stepActive(STEP_1_LABEL))
|
||||
assertThat(captor.lastValue.toString()).doesNotContain(stepActive(STEP_5_LABEL))
|
||||
verify(printWriter, times(4)).flush()
|
||||
checkTrackingState(captor, 4, listOf(stepActive(STEP_1_LABEL), stepNotRun(STEP_4_LABEL), stepNotRun(STEP_5_LABEL)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `duplicate steps in different children handled correctly`() {
|
||||
val captor = argumentCaptor<Ansi>()
|
||||
progressRenderer.render(flowProgressHandle)
|
||||
feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(0, STEP_2_LABEL)))
|
||||
indexSubject.onNext(0)
|
||||
|
||||
checkTrackingState(captor, 1, listOf(stepActive(STEP_1_LABEL), stepNotRun(STEP_2_LABEL)))
|
||||
|
||||
feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(1, STEP_3_LABEL), Pair(0, STEP_2_LABEL), Pair(1, STEP_3_LABEL)))
|
||||
indexSubject.onNext(1)
|
||||
indexSubject.onNext(2)
|
||||
indexSubject.onNext(3)
|
||||
|
||||
checkTrackingState(captor, 5, listOf(stepSuccess(STEP_1_LABEL), stepSuccess(STEP_3_LABEL), stepSuccess(STEP_2_LABEL), stepActive(STEP_3_LABEL)))
|
||||
|
||||
feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(1, STEP_3_LABEL), Pair(0, STEP_2_LABEL), Pair(1, STEP_3_LABEL), Pair(2, STEP_4_LABEL)))
|
||||
|
||||
checkTrackingState(captor, 6, listOf(stepSuccess(STEP_1_LABEL), stepSuccess(STEP_3_LABEL), stepSuccess(STEP_2_LABEL), stepActive(STEP_3_LABEL), stepNotRun(STEP_4_LABEL)))
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user