diff --git a/cmd/demo-progress/demo.go b/cmd/demo-progress/demo.go index 54e9035..a9a69c6 100644 --- a/cmd/demo-progress/demo.go +++ b/cmd/demo-progress/demo.go @@ -39,6 +39,9 @@ func trackSomething(pw progress.Writer, idx int64) { message = fmt.Sprintf("Calculating Total #%3d", idx) } tracker := progress.Tracker{Message: message, Total: total, Units: *units} + if idx == int64(*numTrackers) { + tracker.Total = 0 + } pw.AppendTracker(&tracker) @@ -47,6 +50,9 @@ func trackSomething(pw progress.Writer, idx int64) { select { case <-c: tracker.Increment(incrementPerCycle) + if idx == int64(*numTrackers) && tracker.Value() >= total { + tracker.MarkAsDone() + } } } } diff --git a/progress/indicator.go b/progress/indicator.go new file mode 100644 index 0000000..ba1748c --- /dev/null +++ b/progress/indicator.go @@ -0,0 +1,135 @@ +package progress + +import ( + "time" + + "github.com/jedib0t/go-pretty/v6/text" +) + +// IndeterminateIndicator defines the structure for the indicator to indicate +// indeterminate progress. Ex.: <=> +type IndeterminateIndicator struct { + Position int + Text string +} + +// IndeterminateIndicatorGenerator returns an IndeterminateIndicator for cases +// where the progress percentage cannot be calculated. Ex.: [........<=>....] +type IndeterminateIndicatorGenerator func(maxLen int) IndeterminateIndicator + +// IndeterminateIndicatorMovingBackAndForth returns an instance of +// IndeterminateIndicatorGenerator function that incrementally moves from the +// left to right and back for each specified duration. If duration is 0, then +// every single invocation moves the indicator. +func IndeterminateIndicatorMovingBackAndForth(indicator string, duration time.Duration) IndeterminateIndicatorGenerator { + var indeterminateIndicator *IndeterminateIndicator + indicatorGenerator := indeterminateIndicatorMovingBackAndForth(indicator) + lastRenderTime := time.Now() + + return func(maxLen int) IndeterminateIndicator { + currRenderTime := time.Now() + if indeterminateIndicator == nil || duration == 0 || currRenderTime.Sub(lastRenderTime) > duration { + tmpIndeterminateIndicator := indicatorGenerator(maxLen) + indeterminateIndicator = &tmpIndeterminateIndicator + lastRenderTime = currRenderTime + } + + return *indeterminateIndicator + } +} + +// IndeterminateIndicatorMovingLeftToRight returns an instance of +// IndeterminateIndicatorGenerator function that incrementally moves from the +// left to right and starts from left again for each specified duration. If +// duration is 0, then every single invocation moves the indicator. +func IndeterminateIndicatorMovingLeftToRight(indicator string, duration time.Duration) IndeterminateIndicatorGenerator { + var indeterminateIndicator *IndeterminateIndicator + indicatorGenerator := indeterminateIndicatorMovingLeftToRight(indicator) + lastRenderTime := time.Now() + + return func(maxLen int) IndeterminateIndicator { + currRenderTime := time.Now() + if indeterminateIndicator == nil || duration == 0 || currRenderTime.Sub(lastRenderTime) > duration { + tmpIndeterminateIndicator := indicatorGenerator(maxLen) + indeterminateIndicator = &tmpIndeterminateIndicator + lastRenderTime = currRenderTime + } + + return *indeterminateIndicator + } +} + +// IndeterminateIndicatorMovingRightToLeft returns an instance of +// IndeterminateIndicatorGenerator function that incrementally moves from the +// right to left and starts from right again for each specified duration. If +// duration is 0, then every single invocation moves the indicator. +func IndeterminateIndicatorMovingRightToLeft(indicator string, duration time.Duration) IndeterminateIndicatorGenerator { + var indeterminateIndicator *IndeterminateIndicator + indicatorGenerator := indeterminateIndicatorMovingRightToLeft(indicator) + lastRenderTime := time.Now() + + return func(maxLen int) IndeterminateIndicator { + currRenderTime := time.Now() + if indeterminateIndicator == nil || duration == 0 || currRenderTime.Sub(lastRenderTime) > duration { + tmpIndeterminateIndicator := indicatorGenerator(maxLen) + indeterminateIndicator = &tmpIndeterminateIndicator + lastRenderTime = currRenderTime + } + + return *indeterminateIndicator + } +} + +func indeterminateIndicatorMovingBackAndForth(indicator string) IndeterminateIndicatorGenerator { + increment := 1 + nextPosition := 0 + + return func(maxLen int) IndeterminateIndicator { + currentPosition := nextPosition + if currentPosition == 0 { + increment = 1 + } else if currentPosition+text.RuneCount(indicator) == maxLen { + increment = -1 + } + nextPosition += increment + + return IndeterminateIndicator{ + Position: currentPosition, + Text: indicator, + } + } +} + +func indeterminateIndicatorMovingLeftToRight(indicator string) IndeterminateIndicatorGenerator { + nextPosition := 0 + + return func(maxLen int) IndeterminateIndicator { + currentPosition := nextPosition + nextPosition++ + if nextPosition+text.RuneCount(indicator) > maxLen { + nextPosition = 0 + } + + return IndeterminateIndicator{ + Position: currentPosition, + Text: indicator, + } + } +} + +func indeterminateIndicatorMovingRightToLeft(indicator string) IndeterminateIndicatorGenerator { + nextPosition := -1 + + return func(maxLen int) IndeterminateIndicator { + if nextPosition == -1 { + nextPosition = maxLen - text.RuneCount(indicator) + } + currentPosition := nextPosition + nextPosition-- + + return IndeterminateIndicator{ + Position: currentPosition, + Text: indicator, + } + } +} diff --git a/progress/indicator_test.go b/progress/indicator_test.go new file mode 100644 index 0000000..833aa97 --- /dev/null +++ b/progress/indicator_test.go @@ -0,0 +1,192 @@ +package progress + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestIndeterminateIndicatorMovingBackAndForth(t *testing.T) { + maxLen := 10 + indicator := "<=>" + expectedPositions := []int{ + 0, 1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1, + 0, 1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1, + } + + f := IndeterminateIndicatorMovingBackAndForth(indicator, time.Millisecond*10) + for idx, expectedPosition := range expectedPositions { + actual := f(maxLen) + assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedIndeterminateIndicators[%d]", idx)) + time.Sleep(time.Millisecond * 10) + } +} + +func Test_indeterminateIndicatorMovingBackAndForth1(t *testing.T) { + maxLen := 10 + indicator := "?" + expectedPositions := []int{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, + } + + f := indeterminateIndicatorMovingBackAndForth(indicator) + for idx, expectedPosition := range expectedPositions { + actual := f(maxLen) + assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedIndeterminateIndicators[%d]", idx)) + } +} + +func Test_indeterminateIndicatorMovingBackAndForth2(t *testing.T) { + maxLen := 10 + indicator := "<>" + expectedPositions := []int{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1, + } + + f := indeterminateIndicatorMovingBackAndForth(indicator) + for idx, expectedPosition := range expectedPositions { + actual := f(maxLen) + assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedIndeterminateIndicators[%d]", idx)) + } +} + +func Test_indeterminateIndicatorMovingBackAndForth3(t *testing.T) { + maxLen := 10 + indicator := "<=>" + expectedPositions := []int{ + 0, 1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1, + 0, 1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1, + } + + f := indeterminateIndicatorMovingBackAndForth(indicator) + for idx, expectedPosition := range expectedPositions { + actual := f(maxLen) + assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedIndeterminateIndicators[%d]", idx)) + } +} + +func TestIndeterminateIndicatorMovingLeftToRight(t *testing.T) { + maxLen := 10 + indicator := "?" + expectedPositions := []int{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + } + + f := IndeterminateIndicatorMovingLeftToRight(indicator, time.Millisecond*10) + for idx, expectedPosition := range expectedPositions { + actual := f(maxLen) + assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedIndeterminateIndicators[%d]", idx)) + time.Sleep(time.Millisecond * 10) + } +} + +func Test_indeterminateIndicatorMovingLeftToRight1(t *testing.T) { + maxLen := 10 + indicator := "?" + expectedPositions := []int{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + } + + f := indeterminateIndicatorMovingLeftToRight(indicator) + for idx, expectedPosition := range expectedPositions { + actual := f(maxLen) + assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedIndeterminateIndicators[%d]", idx)) + } +} + +func Test_indeterminateIndicatorMovingLeftToRight2(t *testing.T) { + maxLen := 10 + indicator := "<>" + expectedPositions := []int{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, + 0, 1, 2, 3, 4, 5, 6, 7, 8, + } + + f := indeterminateIndicatorMovingLeftToRight(indicator) + for idx, expectedPosition := range expectedPositions { + actual := f(maxLen) + assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedIndeterminateIndicators[%d]", idx)) + } +} + +func Test_indeterminateIndicatorMovingLeftToRight3(t *testing.T) { + maxLen := 10 + indicator := "<=>" + expectedPositions := []int{ + 0, 1, 2, 3, 4, 5, 6, 7, + 0, 1, 2, 3, 4, 5, 6, 7, + } + + f := indeterminateIndicatorMovingLeftToRight(indicator) + for idx, expectedPosition := range expectedPositions { + actual := f(maxLen) + assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedIndeterminateIndicators[%d]", idx)) + } +} + +func TestIndeterminateIndicatorMovingRightToLeft(t *testing.T) { + maxLen := 10 + indicator := "?" + expectedPositions := []int{ + 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, + 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, + } + + f := IndeterminateIndicatorMovingRightToLeft(indicator, time.Millisecond*10) + for idx, expectedPosition := range expectedPositions { + actual := f(maxLen) + assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedIndeterminateIndicators[%d]", idx)) + time.Sleep(time.Millisecond * 10) + } +} + +func Test_indeterminateIndicatorMovingRightToLeft1(t *testing.T) { + maxLen := 10 + indicator := "?" + expectedPositions := []int{ + 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, + 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, + } + + f := indeterminateIndicatorMovingRightToLeft(indicator) + for idx, expectedPosition := range expectedPositions { + actual := f(maxLen) + assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedIndeterminateIndicators[%d]", idx)) + } +} + +func Test_indeterminateIndicatorMovingRightToLeft2(t *testing.T) { + maxLen := 10 + indicator := "<>" + expectedPositions := []int{ + 8, 7, 6, 5, 4, 3, 2, 1, 0, + 8, 7, 6, 5, 4, 3, 2, 1, 0, + } + + f := indeterminateIndicatorMovingRightToLeft(indicator) + for idx, expectedPosition := range expectedPositions { + actual := f(maxLen) + assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedIndeterminateIndicators[%d]", idx)) + } +} + +func Test_indeterminateIndicatorMovingRightToLeft3(t *testing.T) { + maxLen := 10 + indicator := "<=>" + expectedPositions := []int{ + 7, 6, 5, 4, 3, 2, 1, 0, + 7, 6, 5, 4, 3, 2, 1, 0, + } + + f := indeterminateIndicatorMovingRightToLeft(indicator) + for idx, expectedPosition := range expectedPositions { + actual := f(maxLen) + assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedIndeterminateIndicators[%d]", idx)) + } +} diff --git a/progress/progress.go b/progress/progress.go index 140b3f9..b37f1c3 100644 --- a/progress/progress.go +++ b/progress/progress.go @@ -65,7 +65,7 @@ const ( // to a queue, which gets picked up by the Render logic in the next rendering // cycle. func (p *Progress) AppendTracker(t *Tracker) { - if t.Total <= 0 { + if t.Total < 0 { t.Total = math.MaxInt64 } t.start() diff --git a/progress/progress_test.go b/progress/progress_test.go index 161c8db..b0e3a24 100644 --- a/progress/progress_test.go +++ b/progress/progress_test.go @@ -15,10 +15,15 @@ func TestProgress_AppendTracker(t *testing.T) { tracker := &Tracker{} assert.Equal(t, int64(0), tracker.Total) - p.AppendTracker(tracker) assert.Equal(t, 1, len(p.trackersInQueue)) - assert.Equal(t, int64(math.MaxInt64), tracker.Total) + assert.Equal(t, int64(0), tracker.Total) + + tracker2 := &Tracker{Total: -1} + assert.Equal(t, int64(-1), tracker2.Total) + p.AppendTracker(tracker2) + assert.Equal(t, 2, len(p.trackersInQueue)) + assert.Equal(t, int64(math.MaxInt64), tracker2.Total) } func TestProgress_AppendTrackers(t *testing.T) { diff --git a/progress/render.go b/progress/render.go index 29f045e..f08a57b 100644 --- a/progress/render.go +++ b/progress/render.go @@ -1,6 +1,7 @@ package progress import ( + "fmt" "math" "strings" "time" @@ -119,11 +120,23 @@ func (p *Progress) extractDoneAndActiveTrackers() ([]*Tracker, []*Tracker) { return trackersActive, trackersDone } -func (p *Progress) generateTrackerStr(t *Tracker, maxLen int) string { +func (p *Progress) generateTrackerStr(t *Tracker, maxLen int, hint renderHint) string { + if !hint.isOverallTracker && (t.Total == 0 || t.value > t.Total) { + return p.generateTrackerStrIndeterminate(t, maxLen) + } + return p.generateTrackerStrDeterminate(t, maxLen) +} + +// generateTrackerStrDeterminate generates the tracker string for the case where +// the Total value is known, and the progress percentage can be calculated. +func (p *Progress) generateTrackerStrDeterminate(t *Tracker, maxLen int) string { t.mutex.Lock() + pFinishedDots, pFinishedDotsFraction := 0.0, 0.0 pDotValue := float64(t.Total) / float64(maxLen) - pFinishedDots := float64(t.value) / pDotValue - pFinishedDotsFraction := pFinishedDots - float64(int(pFinishedDots)) + if pDotValue > 0 { + pFinishedDots = float64(t.value) / pDotValue + pFinishedDotsFraction = pFinishedDots - float64(int(pFinishedDots)) + } pFinishedLen := int(math.Floor(pFinishedDots)) t.mutex.Unlock() @@ -151,6 +164,25 @@ func (p *Progress) generateTrackerStr(t *Tracker, maxLen int) string { ) } +// generateTrackerStrDeterminate generates the tracker string for the case where +// the Total value is unknown, and the progress percentage cannot be calculated. +func (p *Progress) generateTrackerStrIndeterminate(t *Tracker, maxLen int) string { + indicator := p.style.Chars.Indeterminate(maxLen) + + pUnfinished := "" + if indicator.Position > 0 { + pUnfinished += strings.Repeat(p.style.Chars.Unfinished, indicator.Position) + } + pUnfinished += indicator.Text + if text.RuneCount(pUnfinished) < maxLen { + pUnfinished += strings.Repeat(p.style.Chars.Unfinished, maxLen-text.RuneCount(pUnfinished)) + } + + return p.style.Colors.Tracker.Sprintf("%s%s%s", + p.style.Chars.BoxLeft, string(pUnfinished), p.style.Chars.BoxRight, + ) +} + func (p *Progress) moveCursorToTheTop(out *strings.Builder) { numLinesToMoveUp := len(p.trackersActive) if p.showOverallTracker && p.overallTracker != nil && !p.overallTracker.IsDone() { @@ -177,14 +209,14 @@ func (p *Progress) renderTracker(out *strings.Builder, t *Tracker, hint renderHi trackerLen += text.RuneCount(p.style.Options.DoneString) trackerLen += p.lengthProgress + 1 hint := renderHint{hideValue: true, isOverallTracker: true} - p.renderTrackerProgress(out, t, p.generateTrackerStr(t, trackerLen), hint) + p.renderTrackerProgress(out, t, p.generateTrackerStr(t, trackerLen, hint), hint) } } else { if t.IsDone() { p.renderTrackerDone(out, t) } else { hint := renderHint{hideTime: p.hideTime, hideValue: p.hideValue} - p.renderTrackerProgress(out, t, p.generateTrackerStr(t, p.lengthProgress), hint) + p.renderTrackerProgress(out, t, p.generateTrackerStr(t, p.lengthProgress, hint), hint) } } } @@ -234,7 +266,13 @@ func (p *Progress) renderTrackerProgress(out *strings.Builder, t *Tracker, track func (p *Progress) renderTrackerPercentage(out *strings.Builder, t *Tracker) { if !p.hidePercentage { - out.WriteString(p.style.Colors.Percent.Sprintf(p.style.Options.PercentFormat, t.PercentDone())) + var percentageStr string + if t.IsIndeterminate() { + percentageStr = p.style.Options.PercentIndeterminate + } else { + percentageStr = fmt.Sprintf(p.style.Options.PercentFormat, t.PercentDone()) + } + out.WriteString(p.style.Colors.Percent.Sprint(percentageStr)) } } diff --git a/progress/render_test.go b/progress/render_test.go index b797862..190b871 100644 --- a/progress/render_test.go +++ b/progress/render_test.go @@ -1,7 +1,9 @@ package progress import ( + "fmt" "regexp" + "sort" "strings" "testing" "time" @@ -41,10 +43,10 @@ func generateWriter() Writer { } func trackSomething(pw Writer, tracker *Tracker) { - pw.AppendTracker(tracker) - incrementPerCycle := tracker.Total / 3 + pw.AppendTracker(tracker) + c := time.Tick(time.Millisecond * 100) for !tracker.IsDone() { select { @@ -58,6 +60,29 @@ func trackSomething(pw Writer, tracker *Tracker) { } } +func trackSomethingIndeterminate(pw Writer, tracker *Tracker) { + incrementPerCycle := tracker.Total / 3 + total := tracker.Total + tracker.Total = 0 + + pw.AppendTracker(tracker) + + c := time.Tick(time.Millisecond * 100) + for !tracker.IsDone() { + select { + case <-c: + if tracker.value+incrementPerCycle > total { + tracker.Increment(total - tracker.value) + } else { + tracker.Increment(incrementPerCycle) + } + if tracker.Value() >= total { + tracker.MarkAsDone() + } + } + } +} + func renderAndWait(pw Writer, autoStop bool) { go pw.Render() time.Sleep(time.Millisecond * 100) @@ -72,6 +97,16 @@ func renderAndWait(pw Writer, autoStop bool) { } } +func showOutputOnFailure(t *testing.T, out string) { + if t.Failed() { + lines := strings.Split(out, "\n") + sort.Strings(lines) + for _, line := range lines { + fmt.Printf("%#v,\n", line) + } + } +} + func TestProgress_generateTrackerStr(t *testing.T) { pw := Progress{} pw.Style().Chars = StyleChars{ @@ -188,13 +223,150 @@ func TestProgress_generateTrackerStr(t *testing.T) { 100: "##########", } + finalOutput := strings.Builder{} tr := Tracker{Total: 100} - for value := int64(0); value <= tr.Total; value++ { + for value := int64(0); value <= 100; value++ { + tr.value = value + actualStr := pw.generateTrackerStr(&tr, 10, renderHint{}) + if expectedStr, ok := expectedTrackerStrMap[value]; ok { + assert.Equal(t, expectedStr, actualStr, "value=%d", value) + } + finalOutput.WriteString(fmt.Sprintf(" %d: \"%s\",\n", value, actualStr)) + } + if t.Failed() { + fmt.Println(finalOutput.String()) + } +} + +func TestProgress_generateTrackerStr_Indeterminate(t *testing.T) { + pw := Progress{} + pw.Style().Chars = StyleChars{ + BoxLeft: "", + BoxRight: "", + Finished: "#", + Finished25: "1", + Finished50: "2", + Finished75: "3", + Indeterminate: indeterminateIndicatorMovingBackAndForth("<=>"), + Unfinished: ".", + } + + expectedTrackerStrMap := map[int64]string{ + 0: "<=>.......", + 1: ".<=>......", + 2: "..<=>.....", + 3: "...<=>....", + 4: "....<=>...", + 5: ".....<=>..", + 6: "......<=>.", + 7: ".......<=>", + 8: "......<=>.", + 9: ".....<=>..", + 10: "....<=>...", + 11: "...<=>....", + 12: "..<=>.....", + 13: ".<=>......", + 14: "<=>.......", + 15: ".<=>......", + 16: "..<=>.....", + 17: "...<=>....", + 18: "....<=>...", + 19: ".....<=>..", + 20: "......<=>.", + 21: ".......<=>", + 22: "......<=>.", + 23: ".....<=>..", + 24: "....<=>...", + 25: "...<=>....", + 26: "..<=>.....", + 27: ".<=>......", + 28: "<=>.......", + 29: ".<=>......", + 30: "..<=>.....", + 31: "...<=>....", + 32: "....<=>...", + 33: ".....<=>..", + 34: "......<=>.", + 35: ".......<=>", + 36: "......<=>.", + 37: ".....<=>..", + 38: "....<=>...", + 39: "...<=>....", + 40: "..<=>.....", + 41: ".<=>......", + 42: "<=>.......", + 43: ".<=>......", + 44: "..<=>.....", + 45: "...<=>....", + 46: "....<=>...", + 47: ".....<=>..", + 48: "......<=>.", + 49: ".......<=>", + 50: "......<=>.", + 51: ".....<=>..", + 52: "....<=>...", + 53: "...<=>....", + 54: "..<=>.....", + 55: ".<=>......", + 56: "<=>.......", + 57: ".<=>......", + 58: "..<=>.....", + 59: "...<=>....", + 60: "....<=>...", + 61: ".....<=>..", + 62: "......<=>.", + 63: ".......<=>", + 64: "......<=>.", + 65: ".....<=>..", + 66: "....<=>...", + 67: "...<=>....", + 68: "..<=>.....", + 69: ".<=>......", + 70: "<=>.......", + 71: ".<=>......", + 72: "..<=>.....", + 73: "...<=>....", + 74: "....<=>...", + 75: ".....<=>..", + 76: "......<=>.", + 77: ".......<=>", + 78: "......<=>.", + 79: ".....<=>..", + 80: "....<=>...", + 81: "...<=>....", + 82: "..<=>.....", + 83: ".<=>......", + 84: "<=>.......", + 85: ".<=>......", + 86: "..<=>.....", + 87: "...<=>....", + 88: "....<=>...", + 89: ".....<=>..", + 90: "......<=>.", + 91: ".......<=>", + 92: "......<=>.", + 93: ".....<=>..", + 94: "....<=>...", + 95: "...<=>....", + 96: "..<=>.....", + 97: ".<=>......", + 98: "<=>.......", + 99: ".<=>......", + 100: "..<=>.....", + } + + finalOutput := strings.Builder{} + tr := Tracker{Total: 0} + for value := int64(0); value <= 100; value++ { tr.value = value - //fmt.Printf(" %5d: \"%s\",\n", value, pw.generateTrackerStr(&tr, 10)) + actualStr := pw.generateTrackerStr(&tr, 10, renderHint{}) if expectedStr, ok := expectedTrackerStrMap[value]; ok { - assert.Equal(t, expectedStr, pw.generateTrackerStr(&tr, 10), "value=%d", value) + assert.Equal(t, expectedStr, actualStr, "value=%d", value) } + finalOutput.WriteString(fmt.Sprintf(" %d: \"%s\",\n", value, actualStr)) + } + if t.Failed() { + fmt.Println(finalOutput.String()) } } @@ -237,6 +409,7 @@ func TestProgress_RenderSomeTrackers_OnLeftSide(t *testing.T) { assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } + showOutputOnFailure(t, out) } func TestProgress_RenderSomeTrackers_OnRightSide(t *testing.T) { @@ -264,6 +437,7 @@ func TestProgress_RenderSomeTrackers_OnRightSide(t *testing.T) { assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } + showOutputOnFailure(t, out) } func TestProgress_RenderSomeTrackers_WithAutoStop(t *testing.T) { @@ -292,6 +466,34 @@ func TestProgress_RenderSomeTrackers_WithAutoStop(t *testing.T) { assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } + showOutputOnFailure(t, out) +} + +func TestProgress_RenderSomeTrackers_WithIndeterminateTracker(t *testing.T) { + renderOutput := outputWriter{} + + pw := generateWriter() + pw.SetOutputWriter(&renderOutput) + go trackSomething(pw, &Tracker{Message: "Calculating Total # 1\r", Total: 1000, Units: UnitsDefault}) + go trackSomething(pw, &Tracker{Message: "Downloading File\t# 2", Total: 1000, Units: UnitsBytes}) + go trackSomethingIndeterminate(pw, &Tracker{Message: "Transferring Amount # 3", Total: 1000, Units: UnitsCurrencyDollar}) + renderAndWait(pw, false) + + expectedOutPatterns := []*regexp.Regexp{ + regexp.MustCompile(`\x1b\[KCalculating Total # 1 \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+ in [\d.]+ms]`), + regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+B in [\d.]+ms]`), + regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. \?\?\? \[[<#>.]{23}] \[\$\d+ in [\d.]+ms]`), + regexp.MustCompile(`\x1b\[KCalculating Total # 1 \.\.\. done! \[\d+\.\d+K in [\d.]+ms]`), + regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. done! \[\d+\.\d+KB in [\d.]+ms]`), + regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. done! \[\$\d+\.\d+K in [\d.]+ms]`), + } + out := renderOutput.String() + for _, expectedOutPattern := range expectedOutPatterns { + if !expectedOutPattern.MatchString(out) { + assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) + } + } + showOutputOnFailure(t, out) } func TestProgress_RenderSomeTrackers_WithLineWidth1(t *testing.T) { @@ -320,6 +522,7 @@ func TestProgress_RenderSomeTrackers_WithLineWidth1(t *testing.T) { assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } + showOutputOnFailure(t, out) } func TestProgress_RenderSomeTrackers_WithLineWidth2(t *testing.T) { @@ -348,6 +551,7 @@ func TestProgress_RenderSomeTrackers_WithLineWidth2(t *testing.T) { assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } + showOutputOnFailure(t, out) } func TestProgress_RenderSomeTrackers_WithOverallTracker(t *testing.T) { @@ -378,6 +582,7 @@ func TestProgress_RenderSomeTrackers_WithOverallTracker(t *testing.T) { assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } + showOutputOnFailure(t, out) } func TestProgress_RenderSomeTrackers_WithoutOverallTracker_WithETA(t *testing.T) { @@ -408,4 +613,5 @@ func TestProgress_RenderSomeTrackers_WithoutOverallTracker_WithETA(t *testing.T) assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } + showOutputOnFailure(t, out) } diff --git a/progress/style.go b/progress/style.go index a00d352..6199879 100644 --- a/progress/style.go +++ b/progress/style.go @@ -50,58 +50,63 @@ var ( // StyleChars defines the characters/strings to use for rendering the Tracker. type StyleChars struct { - BoxLeft string // left-border - BoxRight string // right-border - Finished string // finished block - Finished25 string // 25% finished block - Finished50 string // 50% finished block - Finished75 string // 75% finished block - Unfinished string // 0% finished block + BoxLeft string // left-border + BoxRight string // right-border + Finished string // finished block + Finished25 string // 25% finished block + Finished50 string // 50% finished block + Finished75 string // 75% finished block + Indeterminate IndeterminateIndicatorGenerator + Unfinished string // 0% finished block } var ( // StyleCharsDefault uses simple ASCII characters. StyleCharsDefault = StyleChars{ - BoxLeft: "[", - BoxRight: "]", - Finished: "#", - Finished25: ".", - Finished50: ".", - Finished75: ".", - Unfinished: ".", + BoxLeft: "[", + BoxRight: "]", + Finished: "#", + Finished25: ".", + Finished50: ".", + Finished75: ".", + Indeterminate: IndeterminateIndicatorMovingBackAndForth("<#>", DefaultUpdateFrequency/2), + Unfinished: ".", } // StyleCharsBlocks uses UNICODE Block Drawing characters. StyleCharsBlocks = StyleChars{ - BoxLeft: "║", - BoxRight: "║", - Finished: "█", - Finished25: "░", - Finished50: "▒", - Finished75: "▓", - Unfinished: "░", + BoxLeft: "║", + BoxRight: "║", + Finished: "█", + Finished25: "░", + Finished50: "▒", + Finished75: "▓", + Indeterminate: IndeterminateIndicatorMovingBackAndForth("▒█▒", DefaultUpdateFrequency/2), + Unfinished: "░", } // StyleCharsCircle uses UNICODE Circle characters. StyleCharsCircle = StyleChars{ - BoxLeft: "(", - BoxRight: ")", - Finished: "●", - Finished25: "○", - Finished50: "○", - Finished75: "○", - Unfinished: "◌", + BoxLeft: "(", + BoxRight: ")", + Finished: "●", + Finished25: "○", + Finished50: "○", + Finished75: "○", + Indeterminate: IndeterminateIndicatorMovingBackAndForth("○●○", DefaultUpdateFrequency/2), + Unfinished: "◌", } // StyleCharsRhombus uses UNICODE Rhombus characters. StyleCharsRhombus = StyleChars{ - BoxLeft: "<", - BoxRight: ">", - Finished: "◆", - Finished25: "◈", - Finished50: "◈", - Finished75: "◈", - Unfinished: "◇", + BoxLeft: "<", + BoxRight: ">", + Finished: "◆", + Finished25: "◈", + Finished50: "◈", + Finished75: "◈", + Indeterminate: IndeterminateIndicatorMovingBackAndForth("◈◆◈", DefaultUpdateFrequency/2), + Unfinished: "◇", } ) @@ -120,8 +125,8 @@ var ( // StyleColorsDefault defines sane color choices - None. StyleColorsDefault = StyleColors{} - // StyleColorsExample defines a few choice color options. Use this is just as - // an example to customize the Tracker/text colors. + // StyleColorsExample defines a few choice color options. Use this is just + // as an example to customize the Tracker/text colors. StyleColorsExample = StyleColors{ Message: text.Colors{text.FgWhite}, Percent: text.Colors{text.FgHiRed}, @@ -141,6 +146,7 @@ type StyleOptions struct { Separator string // text between message and tracker SnipIndicator string // text denoting message snipping PercentFormat string // formatting to use for percentage + PercentIndeterminate string // when percentage cannot be computed TimeDonePrecision time.Duration // precision for time when done TimeInProgressPrecision time.Duration // precision for time when in progress TimeOverallPrecision time.Duration // precision for overall time @@ -154,6 +160,7 @@ var ( ETAPrecision: time.Second, ETAString: "~ETA", PercentFormat: "%5.2f%%", + PercentIndeterminate: " ??? ", Separator: " ... ", SnipIndicator: "~", TimeDonePrecision: time.Millisecond, diff --git a/progress/tracker.go b/progress/tracker.go index 1d41527..a911530 100644 --- a/progress/tracker.go +++ b/progress/tracker.go @@ -63,6 +63,15 @@ func (t *Tracker) IsDone() bool { return t.done } +// IsIndeterminate returns true if the tracker is indeterminate; i.e., the total +// is unknown and it is impossible to auto-calculate if tracking is done. +func (t *Tracker) IsIndeterminate() bool { + t.mutex.RLock() + defer t.mutex.RUnlock() + + return t.Total == 0 +} + // MarkAsDone forces completion of the tracker by updating the current value as // the expected Total value. func (t *Tracker) MarkAsDone() { @@ -104,6 +113,13 @@ func (t *Tracker) SetValue(value int64) { t.mutex.Unlock() } +// Value returns the current value of the tracker. +func (t *Tracker) Value() int64 { + t.mutex.Lock() + defer t.mutex.Unlock() + return t.value +} + func (t *Tracker) incrementWithoutLock(value int64) { if !t.done { t.value += value diff --git a/progress/tracker_test.go b/progress/tracker_test.go index afffc91..1683c98 100644 --- a/progress/tracker_test.go +++ b/progress/tracker_test.go @@ -57,6 +57,14 @@ func TestTracker_IsDone(t *testing.T) { assert.True(t, tracker.IsDone()) } +func TestTracker_IsIndeterminate(t *testing.T) { + tracker := Tracker{Total: 10} + assert.False(t, tracker.IsIndeterminate()) + + tracker.Total = 0 + assert.True(t, tracker.IsIndeterminate()) +} + func TestTracker_MarkAsDone(t *testing.T) { tracker := Tracker{} assert.False(t, tracker.IsDone()) @@ -115,3 +123,13 @@ func TestTracker_SetValue(t *testing.T) { assert.Equal(t, tracker.Total, tracker.value) assert.True(t, tracker.done) } + +func TestTracker_Value(t *testing.T) { + tracker := Tracker{} + assert.Equal(t, int64(0), tracker.value) + assert.Equal(t, int64(0), tracker.Value()) + + tracker.SetValue(5) + assert.Equal(t, int64(5), tracker.value) + assert.Equal(t, int64(5), tracker.Value()) +}